1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package net.sf.firemox.clickable.target.card;
22
23 import static net.sf.firemox.clickable.target.card.CardFactory.ACTIVATED_COLOR;
24 import static net.sf.firemox.clickable.target.card.CardFactory.WARNING_PICTURE;
25
26 import java.awt.Component;
27 import java.awt.LayoutManager;
28 import java.awt.event.MouseEvent;
29 import java.awt.event.MouseWheelEvent;
30 import java.awt.event.MouseWheelListener;
31 import java.io.InputStream;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.List;
38 import java.util.Map;
39
40 import javax.swing.JMenuItem;
41 import javax.swing.JSeparator;
42
43 import net.sf.firemox.action.MoveCard;
44 import net.sf.firemox.action.listener.WaitingAbility;
45 import net.sf.firemox.clickable.ability.Ability;
46 import net.sf.firemox.clickable.ability.ActivatedAbility;
47 import net.sf.firemox.clickable.ability.ReplacementAbility;
48 import net.sf.firemox.clickable.ability.UserAbility;
49 import net.sf.firemox.clickable.target.Target;
50 import net.sf.firemox.clickable.target.TargetFactory;
51 import net.sf.firemox.clickable.target.player.Player;
52 import net.sf.firemox.database.DatabaseCard;
53 import net.sf.firemox.database.DatabaseFactory;
54 import net.sf.firemox.deckbuilder.DeckConstraints;
55 import net.sf.firemox.event.ModifiedIdCard;
56 import net.sf.firemox.event.ModifiedIdColor;
57 import net.sf.firemox.event.ModifiedProperty;
58 import net.sf.firemox.event.ModifiedRegister;
59 import net.sf.firemox.event.context.ContextEventListener;
60 import net.sf.firemox.event.context.MContextCardCardIntInt;
61 import net.sf.firemox.modifier.AbilityModifier;
62 import net.sf.firemox.modifier.ColorModifier;
63 import net.sf.firemox.modifier.ControllerModifier;
64 import net.sf.firemox.modifier.IdCardModifier;
65 import net.sf.firemox.modifier.PlayableZoneModifier;
66 import net.sf.firemox.modifier.PropertyModifier;
67 import net.sf.firemox.modifier.RegisterIndirection;
68 import net.sf.firemox.modifier.RegisterModifier;
69 import net.sf.firemox.modifier.model.ModifierModel;
70 import net.sf.firemox.modifier.model.ObjectFactory;
71 import net.sf.firemox.network.ConnectionManager;
72 import net.sf.firemox.network.message.CoreMessageType;
73 import net.sf.firemox.operation.Add;
74 import net.sf.firemox.operation.Operation;
75 import net.sf.firemox.operation.Set;
76 import net.sf.firemox.stack.StackManager;
77 import net.sf.firemox.test.Test;
78 import net.sf.firemox.token.IdCommonToken;
79 import net.sf.firemox.token.IdConst;
80 import net.sf.firemox.token.IdPositions;
81 import net.sf.firemox.token.IdTokens;
82 import net.sf.firemox.token.IdZones;
83 import net.sf.firemox.tools.Log;
84 import net.sf.firemox.tools.MToolKit;
85 import net.sf.firemox.tools.Pair;
86 import net.sf.firemox.ui.Tappable;
87 import net.sf.firemox.ui.i18n.LanguageManager;
88 import net.sf.firemox.ui.layout.AttachmentLayout;
89 import net.sf.firemox.zone.ExpandableZone;
90 import net.sf.firemox.zone.MZone;
91 import net.sf.firemox.zone.ZoneManager;
92
93 import org.apache.commons.lang.ArrayUtils;
94
95 /***
96 * This class corresponds to the graphical element of a specified card, and
97 * corresponds to an actor. When loading a new card, first we take care that a
98 * card with same name has not been already loaded in order to save memory, so
99 * all cards having the same name share the same Image object. <br>
100 *
101 * @since 0.52 "enableReverse" option
102 * @since 0.70 support modifiers
103 * @since 0.71 art and rule author
104 * @since 0.72 support static effects, removed art-author
105 * @since 0.85 property pictures are displayed
106 * @since 0.90 database + card model
107 * @since 0.91 keywords added, externalized to CardModel, explicit attachment
108 * object, and is movable.
109 * @see net.sf.firemox.token.IdCardColors
110 * @author <a href="mailto:fabdouglas@users.sourceforge.net">Fabrice Daugan </a>
111 * @author <a href="mailto:kismet-sl@users.sourceforge.net">Stefano "Kismet"
112 * Lenzi</a>
113 */
114 public class MCard extends AbstractCard implements Tappable, MouseWheelListener {
115
116 /***
117 * Create a new instance of Card reading from a file.
118 *
119 * @param cardName
120 * the card name
121 * @param inputFile
122 * is the file read, containing information
123 * @param controller
124 * is the controller of this card
125 * @param owner
126 * is the owner of this card
127 * @param constraints
128 * the constraints of the card for database management
129 */
130 public MCard(String cardName, InputStream inputFile, Player controller,
131 Player owner, Map<String, String> constraints) {
132 super();
133
134 initUI(null, CardFactory.getCardModel(cardName, inputFile), constraints);
135 initModel();
136 this.copiedCard = null;
137 this.owner = owner;
138 this.controller = controller;
139 this.originalController = controller;
140 lastKnownInstances = new HashMap<Integer, LastKnownCardInfo>();
141 }
142
143 /***
144 *
145 */
146 private void initModel() {
147 final CardModel cardModel = getCardModel();
148
149 registers = cardModel.getStaticRegisters().clone();
150 cachedRegisters = registers.clone();
151
152
153 cachedIdCard = cardModel.getIdCard();
154
155
156 cachedIdColor = cardModel.getIdColor();
157
158
159 cachedAbilities = new ArrayList<Ability>(cardModel.getAbilities().length);
160 for (Ability ability : cardModel.getAbilities()) {
161 cachedAbilities.add(ability.clone(this));
162 }
163
164
165 cachedProperties = new HashSet<Integer>();
166 for (int property : cardModel.getProperties()) {
167 cachedProperties.add(property);
168 }
169
170
171 indirections = new RegisterIndirection[IdTokens.CARD_REGISTER_SIZE];
172 registerModifiers = new RegisterModifier[IdTokens.CARD_REGISTER_SIZE];
173 }
174
175 /***
176 * Create a new instance of Card exactly like cardRef instance. Is called only
177 * from clone method.
178 *
179 * @param cardRef
180 * is the model for this new instance
181 * @param database
182 * the database of this new card.
183 */
184 public MCard(MCard cardRef, DatabaseCard database) {
185
186 this.database = database;
187 this.copiedCard = null;
188
189
190 initUI(null, database.getCardModel(), null);
191 initModel();
192 lastKnownInstances = new HashMap<Integer, LastKnownCardInfo>();
193
194 if (cardRef != null) {
195 controller = cardRef.controller;
196 owner = cardRef.owner;
197 }
198 originalController = controller;
199 }
200
201 /***
202 * Create a new instance of Card tokenized. No field is initialized.
203 */
204
205 protected MCard() {
206 super();
207 cachedAbilities = null;
208 database = null;
209 copiedCard = null;
210 originalController = null;
211 }
212
213 /***
214 * Create a new instance of tokenized Card. This constructor should be called
215 * only to obtain the a copy of the specified card sharing registers and
216 * abilities. Also, any modification done on registers or ability of one of
217 * these cards will be visible on the other. The zone information of cards is
218 * not shared, and the new card will have it's zone initialized with stack
219 * identifier instead of side. The picture used for this card will be the
220 * specified one if it is not null. Otherwise the cardRef picture will be
221 * used.
222 *
223 * @param pictureName
224 * is the picture name used to represent the copy. May be null.
225 * @param cardRef
226 * is the model for this instance
227 * @see IdZones#STACK
228 */
229 public MCard(String pictureName, MCard cardRef) {
230
231 database = cardRef.database;
232
233
234 initUI(pictureName, cardRef.database.getCardModel(), null);
235
236 this.idZone = IdZones.STACK;
237 this.registers = cardRef.registers;
238 this.cachedRegisters = cardRef.cachedRegisters;
239 this.cachedProperties = cardRef.cachedProperties;
240 this.cachedAbilities = null;
241 this.copiedCard = cardRef;
242 this.cachedIdCard = cardRef.cachedIdCard;
243 this.cachedIdColor = cardRef.cachedIdColor;
244 this.indirections = new RegisterIndirection[IdTokens.CARD_REGISTER_SIZE];
245 this.registerModifiers = new RegisterModifier[IdTokens.CARD_REGISTER_SIZE];
246 this.controller = cardRef.controller;
247 this.originalController = controller;
248 this.owner = cardRef.owner;
249 tokenize();
250 }
251
252 @Override
253 public boolean isAbility(int abilityType) {
254 return false;
255 }
256
257 @Override
258 public boolean isSpell() {
259 return true;
260 }
261
262 /***
263 * Indicates whether this card suits to the specified position code.
264 *
265 * @param position
266 * the matching position code
267 * @return true if this card suits to the specified position code.
268 * @see net.sf.firemox.token.IdPositions#ON_THE_BOTTOM
269 * @see net.sf.firemox.token.IdPositions#ON_THE_TOP
270 */
271 public boolean isSamePosition(int position) {
272 return getContainer().isSamePosition(this, position);
273 }
274
275 /***
276 * Return the container of this card.
277 *
278 * @return the container of this card.
279 */
280 public MZone getContainer() {
281 return controller.zoneManager.getContainer(getIdZone());
282 }
283
284 @Override
285 public boolean isACopy() {
286 return copiedCard != null;
287 }
288
289 /***
290 * Indicates if the specified sort of card idCard contains the card's sort.
291 *
292 * @param idCard
293 * are the sorts we match.
294 * @return true if the specified sort idCard contains the card's sort.
295 * @see #hasIdCard(int,int)
296 */
297 public boolean hasIdCard(int idCard) {
298 return hasIdCard(getIdCard(), idCard);
299 }
300
301 /***
302 * Indicates if idCardBIG contains completely idCardMatched. <br>
303 *
304 * @param idCardBIG
305 * are the set of types.
306 * @param idCardMatched
307 * are the type we match.
308 * @return true if idCardBIG contains completely idCardMatched. <br>
309 */
310 public static boolean hasIdCard(int idCardBIG, int idCardMatched) {
311 return (idCardBIG & idCardMatched) == idCardMatched;
312 }
313
314 /***
315 * Indicates if idCardBIG contains partially idCardMatched. <br>
316 *
317 * @param idCardBIG
318 * are the set of types.
319 * @param idCardMatched
320 * are the type we match.
321 * @return true if idCardBIG contains partially idCardMatched. <br>
322 */
323 public static boolean intersectionIdCard(int idCardBIG, int idCardMatched) {
324 return (idCardBIG & idCardMatched) != 0;
325 }
326
327 /***
328 * Indicates if propertyBIG contains completely propertyMatched. <br>
329 *
330 * @param propertyBIG
331 * are the set of properties.
332 * @param propertyMatched
333 * is the property we match.
334 * @return true if propertyBIG contains completely propertyMatched. <br>
335 */
336 public static boolean hasProperty(int[] propertyBIG, int propertyMatched) {
337 return ArrayUtils.contains(propertyBIG, propertyMatched);
338 }
339
340 /***
341 * Indicates if the specified color idColor contains the card's colors. <br>
342 *
343 * @param idColor
344 * are the colors we match.
345 * @return true if the specified color idColor contains the card's colors.
346 * @see #hasIdColor(int,int)
347 */
348 public boolean hasIdColor(int idColor) {
349 return hasIdColor(getIdColor(), idColor);
350 }
351
352 /***
353 * Indicates if the specified color idColorBIG contains the other specified
354 * color idColorMatched. <br>
355 *
356 * @param idColorBIG
357 * are the colors that idColorMatched must have
358 * @param idColorMatched
359 * are the colors we match.
360 * @return true if the specified color idColorBIG contains the other specified
361 * color idColorMatched.
362 */
363 public static boolean hasIdColor(int idColorBIG, int idColorMatched) {
364 return (idColorBIG & idColorMatched) == idColorMatched;
365 }
366
367 /***
368 * <ul>
369 * Indicates if this card match with the specified place and constraint. As
370 * the specified place is an integer, it may contain another information about
371 * constraint in the high bits : xxyy
372 * <li>xx represents the constraint, may be : "must be untapped" or "must be
373 * tapped".
374 * <li>yy represents the zone identifier.
375 * </ul>
376 * The zones are compared, then the constraint tapped/untapped is checked.
377 * <br>
378 * If <code>zoneConstaint</code> is ID__ANYWHERE, return true. <br>
379 * If <code>zoneConstaint</code> is same as current zone of this card and
380 * the constraint is validated, return true <br>
381 *
382 * @param zoneConstaint
383 * the zone id and optional tapped information.
384 * @return true if <code>zoneConstaint</code> is ID__ANYWHERE or
385 * <code>zoneConstaint</code> is same as current place of this card
386 * and the constraint is verified.
387 * @see IdZones#PLAY_TAPPED
388 * @see IdZones#PLAY_UNTAPPED
389 * @see IdZones
390 */
391 public boolean isSameState(int zoneConstaint) {
392 switch (zoneConstaint) {
393 case IdZones.PLAY_TAPPED:
394 return idZone == IdZones.PLAY && tapped;
395 case IdZones.PLAY_UNTAPPED:
396 return idZone == IdZones.PLAY && !tapped;
397 default:
398 return idZone == zoneConstaint;
399 }
400 }
401
402 /***
403 * Compare a zone with the current card'szone
404 *
405 * @param idZone
406 * the other zone
407 * @return true the specified zone, and the current card's zone are the same.
408 */
409 public boolean isSameIdZone(int idZone) {
410 return isSameIdZone(this.idZone, idZone);
411 }
412
413 /***
414 * @param idZone
415 * the other zone
416 * @param other
417 * another zone
418 * @return true the specified zones are the same.
419 */
420 public static boolean isSameIdZone(int idZone, int other) {
421 return getIdZone(idZone, null) == getIdZone(other, null);
422 }
423
424 /***
425 * Return the zone identifier of this card.
426 *
427 * @see IdZones
428 * @return the zone identifier of this card. IdZones#PLAY, IdZones#HAND,...
429 * @see IdZones
430 */
431 public int getIdZone() {
432 return idZone;
433 }
434
435 /***
436 * Return the place where is this card.
437 *
438 * @param idZone
439 * the zone identifier This identifier may contain staus information
440 * like "tapped", "untapped"
441 * @param context
442 * The optional context attached to the request.
443 * @return the place identifier corresponding to the specified zone, and with
444 * any status information
445 * @see IdZones
446 */
447 public static int getIdZone(int idZone, ContextEventListener context) {
448 if (idZone == IdZones.CONTEXT && context != null) {
449
450 return context.getZoneContext();
451 }
452 return idZone & 0x0F;
453 }
454
455 /***
456 * Return a zone code representing the zone and the state of this card.
457 *
458 * @param idZone
459 * the zone identifier without status information like "tapped",
460 * "untapped"
461 * @param tapped
462 * indicate the state of this car.
463 * @return the place identifier corresponding to the specified zone, and with
464 * any status information
465 * @see IdZones
466 */
467 public static int getIdZone(int idZone, boolean tapped) {
468 return idZone == IdZones.PLAY ? tapped ? IdZones.PLAY_TAPPED
469 : IdZones.PLAY_UNTAPPED : idZone;
470 }
471
472 /***
473 * returns all IdColors of this card
474 *
475 * @return all IdColors of this card
476 * @see net.sf.firemox.token.IdCardColors
477 */
478 public int getIdColor() {
479 return cachedIdColor;
480 }
481
482 /***
483 * returns all IdCards of this card
484 *
485 * @return all IdCards of this card
486 */
487 public int getIdCard() {
488 return cachedIdCard;
489 }
490
491 /***
492 * Indicates if this card has this property.
493 *
494 * @param property
495 * is the type required
496 * @return true if this card has the required property.
497 */
498 public boolean hasProperty(int property) {
499 if (cachedProperties == null) {
500 return false;
501 }
502 if (property < DeckConstraints.getMinProperty()
503 || property > DeckConstraints.getMaxProperty()) {
504 return cachedProperties.contains(property);
505 }
506 return cachedProperties.contains(property)
507 || cachedProperties.contains(DeckConstraints.MASTER);
508 }
509
510 /***
511 * indicates if this card has this idType.
512 *
513 * @param idType
514 * is the type required
515 * @param creator
516 * the card that has created the modifier to ignore.
517 * @return true if this card has the required type
518 */
519 public boolean hasPropertyNotFromCreator(int idType, MCard creator) {
520 boolean found;
521 int[] properties = getCardModel().getProperties();
522 found = properties != null && Arrays.binarySearch(properties, idType) >= 0;
523 if (creator == this) {
524 found = false;
525 }
526 if (propertyModifier != null) {
527 return propertyModifier.hasPropertyNotFromCreator(idType, found, creator);
528 }
529 return found;
530 }
531
532 @Override
533 public boolean needReverse() {
534 if (getParent() != null) {
535 if (isAttached()) {
536 return !StackManager.isYou(((MCard) getParent()).controller);
537 }
538 switch (getIdZone()) {
539 case IdZones.STACK:
540 case IdZones.DELAYED:
541 case IdZones.TRIGGERED:
542 case IdZones.SIDE:
543 return false;
544 default:
545 return !StackManager.isYou(controller);
546 }
547 }
548 return !StackManager.isYou(controller);
549 }
550
551 @Override
552 public void moveCard(int destinationZone, Player newController,
553 boolean newIsTapped, int idPosition) {
554
555
556 if (timestampReferences > 0) {
557
558 lastKnownInstances.put(timeStamp, new LastKnownCardInfoImpl(this,
559 timestampReferences, destinationZone));
560 timestampReferences = 0;
561 }
562 boolean fromPlayToPlay = idZone == IdZones.PLAY
563 && destinationZone == IdZones.PLAY;
564
565
566 if (idZone == IdZones.PLAY) {
567 if (!fromPlayToPlay) {
568
569 clearDamages();
570 }
571
572 final MCard attachedTo = isAttached() ? (MCard) getParent() : null;
573 controller.zoneManager.play.remove(this);
574
575 if (!fromPlayToPlay) {
576 tap(false);
577 }
578
579 if (attachedTo != null) {
580
581 attachedTo.ui.updateLayout();
582 }
583 controller.zoneManager.play.repaint();
584 } else if (getParent() != null) {
585 getContainer().remove(this);
586 }
587
588
589 if (controller == null) {
590 owner = newController;
591 }
592 controller = newController;
593 idZone = destinationZone;
594 timeStamp++;
595 isHighLighted = false;
596 reversed = needReverse();
597 clearPrivateNamedObject();
598 ui.updateLayout();
599
600
601 switch (destinationZone) {
602 case IdZones.PLAY:
603
604 if (!fromPlayToPlay) {
605 int[] staticregisters = getCardModel().getStaticRegisters();
606 System.arraycopy(staticregisters, 0, registers, 0,
607 staticregisters.length);
608 }
609
610 tap(newIsTapped);
611 newController.zoneManager.play.addBottom(this);
612 break;
613 case IdZones.NOWHERE:
614
615 break;
616 default:
617 switch (idPosition) {
618 case IdPositions.ON_THE_TOP:
619 newController.zoneManager.getContainer(destinationZone).addTop(this);
620 break;
621 case IdPositions.ON_THE_BOTTOM:
622 default:
623 newController.zoneManager.getContainer(destinationZone).addBottom(this);
624 }
625 }
626 }
627
628 /***
629 * Indicates whether the card with the specified zone identifier is tapped or
630 * not.
631 *
632 * @param idZone
633 * the zone id + tapped position information.
634 * @return true if the card with the specified zone identifier is tapped or
635 * not.
636 */
637 public static boolean isTapped(int idZone) {
638 return (idZone & IdZones.PLAY_TAPPED) == IdZones.PLAY_TAPPED;
639 }
640
641 /***
642 * Register abilities of this card, supposing card is in the specified zone.
643 *
644 * @param zone
645 * The supposed zone this card will go to
646 */
647 public void registerAbilities(int zone) {
648 for (Ability ability : cachedAbilities) {
649 if (ability.eventComing().isWellPlaced(zone)) {
650 ability.registerToManager();
651 }
652 }
653 }
654
655 /***
656 * Return a default ability associated to this card. There is no guarantee
657 * this method returns always the same ability.
658 *
659 * @return a default ability associated to this card.
660 */
661 public Ability getDummyAbility() {
662 if (cachedAbilities == null || cachedAbilities.isEmpty())
663
664 return null;
665 return cachedAbilities.get(0);
666 }
667
668 /***
669 * Register abilities of this card, supposing card is in the specified zone.
670 *
671 * @param zone
672 * The supposed zone this card will go to
673 */
674 public void registerReplacementAbilities(int zone) {
675 for (Ability ability : cachedAbilities) {
676 if (ability instanceof ReplacementAbility
677 && ability.eventComing().isWellPlaced(zone)) {
678 ability.registerToManager();
679 }
680 }
681 }
682
683 /***
684 * Unregister useless abilities from the eventManager.
685 */
686 public void unregisterAbilities() {
687 for (Ability ability : cachedAbilities) {
688 if (!ability.eventComing().isWellPlaced()) {
689 ability.removeFromManager();
690 }
691 }
692 }
693
694 /***
695 * Initialize card picture, database, layout and zone location (SIDE). If the
696 * specified card picture is null, the picture associated to the model will be
697 * used.
698 *
699 * @param pictureName
700 * the picture name used to represent the copy. May be null.
701 * @param cardModel
702 * rules author of this card
703 * @param constraints
704 * the constraints of the card
705 */
706 private void initUI(String pictureName, CardModel cardModel,
707 Map<String, String> constraints) {
708 isHighLighted = false;
709 idZone = IdZones.SIDE;
710
711
712 if (database == null) {
713 database = DatabaseFactory.getDatabase(pictureName, cardModel,
714 constraints);
715 }
716 this.originalDatabase = database;
717 setName(cardModel.getCardName());
718
719 ui = new VirtualCard(this);
720 add(ui);
721 setLayout(new AttachmentLayout());
722 ui.updateSizes();
723 addMouseWheelListener(this);
724 }
725
726 public void tap(boolean tapped) {
727 if (this.tapped != tapped) {
728 this.tapped = tapped;
729
730 for (int i = getComponentCount(); i-- > 0;) {
731 ((Tappable) getComponent(i)).tap(tapped);
732 }
733 ui.updateLayout();
734 }
735 }
736
737 @Override
738 public void reverse(boolean reversed) {
739 super.reverse(reversed);
740 ui.updateLayout();
741 }
742
743 @Override
744 public void highLight(boolean... highlightedZones) {
745 super.highLight(ACTIVATED_COLOR);
746 switch (getIdZone()) {
747 case IdZones.SIDE:
748 case IdZones.STACK:
749 highlightedZones[idZone] = true;
750 break;
751 default:
752 highlightedZones[controller.idPlayer * IdZones.NB_ZONE + idZone] = true;
753 }
754 }
755
756 @Override
757 public void targetize(boolean... highlightedZones) {
758 super.highLight(TargetFactory.TARGET_COLOR);
759 switch (getIdZone()) {
760 case IdZones.SIDE:
761 case IdZones.STACK:
762 highlightedZones[idZone] = true;
763 break;
764 default:
765 highlightedZones[controller.idPlayer * IdZones.NB_ZONE + idZone] = true;
766 }
767 }
768
769 public void mouseWheelMoved(MouseWheelEvent e) {
770
771 if (getIdZone() == IdZones.PLAY && getComponentCount() > 1) {
772 final LayoutManager layoutManager = getLayout();
773 if (layoutManager != null && layoutManager instanceof AttachmentLayout
774 && e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
775 if (e.getWheelRotation() < 0) {
776 ((AttachmentLayout) layoutManager).decreaseCardLayout(-e
777 .getWheelRotation());
778 } else {
779 ((AttachmentLayout) layoutManager).increaseCardLayout(e
780 .getWheelRotation());
781 }
782 }
783 doLayout();
784 getParent().doLayout();
785 getContainer().doLayout();
786 }
787 }
788
789 @Override
790 public void mouseClicked(MouseEvent e) {
791 if (!(getParent() instanceof MZone) && !(getParent() instanceof MCard)) {
792 return;
793 }
794 StackManager.noReplayToken.take();
795 MZone container = getContainer();
796 try {
797 TargetFactory.triggerTargetable = this;
798 if (ConnectionManager.isConnected()
799 && e.getButton() == MouseEvent.BUTTON1) {
800
801 if (!StackManager.actionManager.clickOn(this)) {
802
803 if (!(StackManager.actionManager.currentAction instanceof WaitingAbility)) {
804
805 if (container instanceof ExpandableZone) {
806 ((ExpandableZone) container).toggle();
807 }
808 return;
809 }
810 final List<Ability> abilities = ((WaitingAbility) StackManager.actionManager.currentAction)
811 .abilitiesOf(this);
812 final List<Ability> advAbilities = ((WaitingAbility) StackManager.actionManager.currentAction)
813 .advancedAbilitiesOf(this);
814
815
816 if ((abilities == null || abilities.isEmpty())
817 && (advAbilities == null || advAbilities.isEmpty())) {
818
819 if (container instanceof ExpandableZone) {
820 ((ExpandableZone) container).toggle();
821 }
822 return;
823 }
824
825
826 if (advAbilities != null && !advAbilities.isEmpty()
827 || abilities != null && abilities.size() > 1) {
828
829
830
831 TargetFactory.abilitiesMenu.removeAll();
832 if (abilities != null) {
833 for (Ability ability : abilities) {
834 final JMenuItem item = new JMenuItem("<html>"
835 + ability.toHtmlString(null) + "</html>");
836 item.addActionListener(this);
837 TargetFactory.abilitiesMenu.add(item);
838 }
839 }
840 if (advAbilities != null && !advAbilities.isEmpty()) {
841 TargetFactory.abilitiesMenu.add(new JSeparator());
842 for (Ability ability : advAbilities) {
843 final JMenuItem item = new JMenuItem("<html>"
844 + ability.toHtmlString(null) + "</html>", WARNING_PICTURE);
845 item.addActionListener(this);
846 TargetFactory.abilitiesMenu.add(item);
847 }
848 }
849
850 TargetFactory.abilitiesMenu.show(e.getComponent(), e.getX(), e
851 .getY());
852 return;
853 } else if (abilities != null && abilities.size() == 1
854 && (advAbilities == null || advAbilities.isEmpty())) {
855
856 ((UserAbility) abilities.get(0)).mouseClicked(0);
857 }
858 } else if (StackManager.idHandedPlayer != 0) {
859 if (container instanceof ExpandableZone) {
860 ((ExpandableZone) container).toggle();
861 }
862 } else {
863
864 sendClickToOpponent();
865 StackManager.actionManager.succeedClickOn(this);
866 }
867 } else if (ConnectionManager.isConnected()) {
868
869 CardFactory.contextMenu.removeAll();
870 TargetFactory.triggerTargetable = this;
871
872
873 CardFactory.countItem.setText(LanguageManager.getString("countcard",
874 container.toString(), String.valueOf(container.getCardCount())));
875 CardFactory.contextMenu.add(CardFactory.countItem);
876
877
878 CardFactory.contextMenu.add(CardFactory.reloadPictureItem);
879
880
881 if (container instanceof ExpandableZone) {
882 if (container == ZoneManager.expandedZone) {
883 CardFactory.contextMenu.add(CardFactory.gatherItem);
884 } else {
885 CardFactory.contextMenu.add(CardFactory.expandItem);
886 }
887 }
888 CardFactory.contextMenu.add(new JSeparator());
889
890
891 CardFactory.contextMenu.add(CardFactory.databaseCardInfoItem);
892
893
894
895
896
897
898
899 CardFactory.contextMenu.show(e.getComponent(), e.getX(), e.getY());
900 }
901 } catch (Throwable t) {
902 t.printStackTrace();
903 } finally {
904 StackManager.noReplayToken.release();
905 }
906 }
907
908 @Override
909 public void sendClickToOpponent() {
910 ConnectionManager.send(CoreMessageType.CLICK_CARD, getBytes());
911 }
912
913 /***
914 * Return the data array representing this card. This array can be send throw
915 * network.
916 *
917 * @return bytes representing this card.
918 */
919 public final byte[] getBytes() {
920 Pair<Integer, Integer> index = controller.zoneManager.getContainer(idZone)
921 .getRealIndexOf(this);
922
923 return new byte[] { (byte) (1 - controller.idPlayer), (byte) getIdZone(),
924 (byte) index.key.intValue(), (byte) index.value.intValue() };
925 }
926
927 /***
928 * This method is invoked when opponent has clicked on this object.
929 *
930 * @param data
931 * data sent by opponent.
932 */
933 public static void clickOn(byte[] data) {
934
935 MCard card = getCard(data);
936 StackManager.actionManager.clickOn(card);
937 StackManager.actionManager.succeedClickOn(card);
938 }
939
940 /***
941 * Return the component from information read from opponent.
942 *
943 * @param data
944 * data sent by opponent.
945 * @return the card read from the specified input stream
946 */
947 public static MCard getCard(byte[] data) {
948
949 int idPlayer = data[0];
950 int idZone = data[1];
951 int index = data[2];
952 int childIndex = data[3];
953 if (childIndex == 0) {
954
955 return StackManager.PLAYERS[idPlayer].zoneManager.getContainer(idZone)
956 .getCard(index);
957 }
958
959 return (MCard) StackManager.PLAYERS[idPlayer].zoneManager.getContainer(
960 idZone).getCard(index).getComponent(childIndex);
961 }
962
963 @Override
964 public int getValue(int index) {
965 if (index == IdTokens.MANA_POOL) {
966 if (idZone == IdZones.STACK) {
967
968 return StackManager.getTotalManaPaid(this);
969 }
970 return MToolKit.manaPool(cachedRegisters);
971 }
972 if (index == IdTokens.ID) {
973 return getId();
974 }
975
976 if (index >= IdTokens.FIRST_FREE_CARD_INDEX) {
977 return registers[index];
978 }
979 if (cachedRegisters == null) {
980 throw new InternalError("modifiedRegisters is null in getValue in card");
981 }
982 return cachedRegisters[index];
983 }
984
985 @Override
986 public int getValueIndirection(int index) {
987
988 if (indirections[index] != null) {
989 return indirections[index].getValue(registers[index]);
990 }
991 return registers[index];
992 }
993
994 /***
995 * Add a modifier to this object
996 *
997 * @param modifier
998 * the color-modifier to add to this object
999 */
1000 public void addModifier(ColorModifier modifier) {
1001 if (colorModifier != null) {
1002 colorModifier = (ColorModifier) colorModifier.addModifier(modifier);
1003 } else {
1004 colorModifier = modifier;
1005 }
1006 }
1007
1008 /***
1009 * Add a modifier to this object
1010 *
1011 * @param modifier
1012 * the id card modifier to add to this object
1013 */
1014 public void addModifier(IdCardModifier modifier) {
1015 if (idCardModifier != null) {
1016 idCardModifier = (IdCardModifier) idCardModifier.addModifier(modifier);
1017 } else {
1018 idCardModifier = modifier;
1019 }
1020 }
1021
1022 /***
1023 * Add a modifier to this object
1024 *
1025 * @param modifier
1026 * the ability-modifier to add to this object
1027 */
1028 public void addModifier(AbilityModifier modifier) {
1029 if (abilityModifier != null) {
1030 abilityModifier = (AbilityModifier) abilityModifier.addModifier(modifier);
1031 } else {
1032 abilityModifier = modifier;
1033 }
1034 }
1035
1036 /***
1037 * Add a modifier to this object
1038 *
1039 * @param modifier
1040 * the property-modifier to add to this object
1041 */
1042 public void addModifier(PropertyModifier modifier) {
1043 if (propertyModifier != null) {
1044 propertyModifier = (PropertyModifier) propertyModifier
1045 .addModifier(modifier);
1046 } else {
1047 propertyModifier = modifier;
1048 }
1049 }
1050
1051 /***
1052 * Add a modifier to this object
1053 *
1054 * @param modifier
1055 * the controller-modifier to add to this object
1056 */
1057 public void addModifier(ControllerModifier modifier) {
1058 if (controllerModifier != null) {
1059 controllerModifier = (ControllerModifier) controllerModifier
1060 .addModifier(modifier);
1061 } else {
1062 controllerModifier = modifier;
1063 }
1064 }
1065
1066 /***
1067 * Add a modifier to this object
1068 *
1069 * @param modifier
1070 * the playable zone-modifier to add to this object
1071 */
1072 public void addModifier(PlayableZoneModifier modifier) {
1073 if (playableZoneModifier != null) {
1074 playableZoneModifier = (PlayableZoneModifier) playableZoneModifier
1075 .addModifier(modifier);
1076 } else {
1077 playableZoneModifier = modifier;
1078 }
1079 }
1080
1081 @Override
1082 public void removeModifier(RegisterModifier modifier, int index) {
1083 if (registerModifiers[index] != null) {
1084 registerModifiers[index] = (RegisterModifier) registerModifiers[index]
1085 .removeModifier(modifier);
1086 } else {
1087 Log.warn("Card moved, unable to remove a null modifier");
1088 }
1089
1090 }
1091
1092 @Override
1093 public void removeModifier(RegisterIndirection indirection, int index) {
1094 if (indirections[index] != null) {
1095 indirections[index] = (RegisterIndirection) indirections[index]
1096 .removeModifier(indirection);
1097 } else {
1098 Log.warn("Card moved, unable to remove a null modifier");
1099 }
1100 }
1101
1102 /***
1103 * Remove the specified id card modifier
1104 *
1105 * @param modifier
1106 * the id card modifier to be removed from this object
1107 */
1108 public void removeModifier(IdCardModifier modifier) {
1109 if (modifier != null) {
1110 idCardModifier = (IdCardModifier) idCardModifier.removeModifier(modifier);
1111 } else {
1112 Log.warn("Card moved, unable to remove a null modifier");
1113 }
1114 }
1115
1116 /***
1117 * Remove the specified ability-modifier
1118 *
1119 * @param modifier
1120 * the ability-modifier to be removed from this object
1121 */
1122 public void removeModifier(AbilityModifier modifier) {
1123 if (modifier != null) {
1124 abilityModifier = (AbilityModifier) abilityModifier
1125 .removeModifier(modifier);
1126 } else {
1127 Log.warn("Card moved, unable to remove a null modifier");
1128 }
1129 }
1130
1131 /***
1132 * Remove the specified controller-modifier
1133 *
1134 * @param modifier
1135 * the controller-modifier to be removed from this object
1136 */
1137 public void removeModifier(ControllerModifier modifier) {
1138 if (modifier != null) {
1139 controllerModifier = (ControllerModifier) controllerModifier
1140 .removeModifier(modifier);
1141 } else {
1142 Log.warn("Card moved, unable to remove a null modifier");
1143 }
1144 }
1145
1146 /***
1147 * Remove the specified property-modifier
1148 *
1149 * @param modifier
1150 * the property-modifier to be removed from this object
1151 */
1152 public void removeModifier(PropertyModifier modifier) {
1153 if (modifier != null) {
1154 propertyModifier = (PropertyModifier) propertyModifier
1155 .removeModifier(modifier);
1156 } else {
1157 Log.warn("Card moved, unable to remove a null modifier");
1158 }
1159 }
1160
1161 /***
1162 * Remove the specified playable zone-modifier
1163 *
1164 * @param modifier
1165 * the playable zone-modifier to be removed from this object
1166 */
1167 public void removeModifier(PlayableZoneModifier modifier) {
1168 if (modifier != null) {
1169 playableZoneModifier = (PlayableZoneModifier) playableZoneModifier
1170 .removeModifier(modifier);
1171 } else {
1172 Log.warn("Card moved, unable to remove a null modifier");
1173 }
1174 }
1175
1176 /***
1177 * Remove the specified color-modifier.
1178 *
1179 * @param modifier
1180 * the color-modifier to be removed from this object
1181 */
1182 public void removeModifier(ColorModifier modifier) {
1183 if (modifier != null) {
1184 colorModifier = (ColorModifier) colorModifier.removeModifier(modifier);
1185 } else {
1186 Log.warn("Card moved, unable to remove a null modifier");
1187 }
1188 }
1189
1190 /***
1191 * Indicated if this card is attached or not.
1192 *
1193 * @return true value if this card is attached.
1194 */
1195 public boolean isAttached() {
1196 return getParent() != null && getParent() instanceof MCard;
1197 }
1198
1199 @Override
1200 public String getTooltipString() {
1201 return ui.getTooltipString();
1202 }
1203
1204 /***
1205 * Refresh the id card of this card, and raise an event if the value has been
1206 * changed.
1207 */
1208 public void refreshIdCard() {
1209 int modifiedIdCard = idCardModifier != null ? idCardModifier
1210 .getIdCard(getCardModel().getIdCard()) : getCardModel().getIdCard();
1211 if (this.cachedIdCard != modifiedIdCard) {
1212
1213 final int modifiedIdCardTmp = this.cachedIdCard;
1214 this.cachedIdCard = modifiedIdCard;
1215 ModifiedIdCard.dispatchEvent(this, modifiedIdCard | modifiedIdCardTmp);
1216 ui.resetCachedData();
1217 }
1218 }
1219
1220 /***
1221 * Refresh the granted abilities of this card. No event raised.
1222 */
1223 public void refreshAbilities() {
1224 if (abilityModifier == null) {
1225 registerAbilities(idZone);
1226 } else {
1227 final List<Ability> toRegister = new ArrayList<Ability>(cachedAbilities
1228 .size() + 2);
1229
1230 for (Ability ability : cachedAbilities) {
1231 if (ability.eventComing().isWellPlaced(idZone)) {
1232 toRegister.add(ability);
1233 }
1234 }
1235
1236 abilityModifier.calculateDeltaAbilities(toRegister);
1237
1238
1239 for (Ability ability : cachedAbilities) {
1240 if (!toRegister.contains(ability)) {
1241
1242 ability.removeFromManager();
1243 }
1244 }
1245
1246
1247 for (Ability ability : toRegister) {
1248 ability.registerToManager();
1249 }
1250 }
1251 }
1252
1253 /***
1254 * Refresh the colors of this card, and raise an event if the value has been
1255 * changed.
1256 */
1257 public void refreshIdColor() {
1258 int modifiedIdColor = colorModifier != null ? colorModifier
1259 .getIdColor(getCardModel().getIdColor()) : getCardModel().getIdColor();
1260 if (this.cachedIdColor != modifiedIdColor) {
1261
1262 int modifiedIdColorTmp = this.cachedIdColor;
1263 this.cachedIdColor = modifiedIdColor;
1264 ModifiedIdColor.dispatchEvent(this, modifiedIdColor | modifiedIdColorTmp);
1265 this.cachedIdColor = modifiedIdColor;
1266 ui.resetCachedData();
1267 }
1268 }
1269
1270 /***
1271 * Refresh the properties of this card, and raise an event if the value has
1272 * been changed. NOTE : there is no cache for properties.
1273 *
1274 * @param property
1275 * the property to refresh.
1276 */
1277 public void refreshProperties(int property) {
1278 final CardModel cardModel = getCardModel();
1279 boolean raised = false;
1280 if (property == IdConst.ALL) {
1281
1282 java.util.Set<Integer> oldProperties = new HashSet<Integer>(
1283 cachedProperties);
1284 cachedProperties.clear();
1285 for (int initProperty : cardModel.getProperties()) {
1286 cachedProperties.add(initProperty);
1287 }
1288 if (propertyModifier != null) {
1289 propertyModifier.fillProperties(cachedProperties);
1290 }
1291 for (Integer cacheProperty : cachedProperties) {
1292 if (!oldProperties.contains(cacheProperty)) {
1293 ModifiedProperty.dispatchEvent(this, cacheProperty);
1294 raised = true;
1295 }
1296 }
1297 for (Integer oldProperty : oldProperties) {
1298 if (!cachedProperties.contains(oldProperty)) {
1299 ModifiedProperty.dispatchEvent(this, oldProperty);
1300 raised = true;
1301 }
1302 }
1303 } else {
1304 final boolean oldFound = cachedProperties.contains(property);
1305 int[] properties = cardModel.getProperties();
1306 boolean found = properties != null
1307 && Arrays.binarySearch(properties, property) >= 0;
1308 if (propertyModifier != null) {
1309 found = propertyModifier.hasProperty(property, found);
1310 }
1311 if (oldFound != found) {
1312 if (found) {
1313 cachedProperties.add(property);
1314 } else {
1315 cachedProperties.remove(property);
1316 }
1317 ModifiedProperty.dispatchEvent(this, property);
1318 raised = true;
1319 }
1320 }
1321 if (raised) {
1322 ui.resetCachedData();
1323 repaint();
1324 }
1325 }
1326
1327 /***
1328 * Refresh the registers of this card, and raise an event if the value has
1329 * been changed.
1330 *
1331 * @param index
1332 * is the register index to refresh
1333 */
1334 public void refreshRegisters(int index) {
1335 if (index == IdConst.ALL) {
1336 for (int i = 0; i < registerModifiers.length; i++) {
1337 refreshRegisters(i);
1338 }
1339 } else {
1340 final int modifiedRegister;
1341 if (registerModifiers[index] != null)
1342 modifiedRegister = registerModifiers[index]
1343 .getValue(getValueIndirection(index));
1344 else {
1345 modifiedRegister = getValueIndirection(index);
1346 }
1347 if (this.cachedRegisters[index] != modifiedRegister) {
1348
1349
1350 cachedRegisters[index] = modifiedRegister < 0 ? 0 : modifiedRegister;
1351 ModifiedRegister.dispatchEvent(this, this, IdTokens.CARD, index, Set
1352 .getInstance(), modifiedRegister);
1353 ui.resetCachedData();
1354 repaint();
1355 }
1356 }
1357 }
1358
1359 /***
1360 * Refresh the controller of this card, and raise an event if the value has
1361 * been changed.
1362 */
1363 public void refreshController() {
1364 final Player controller = controllerModifier != null ? controllerModifier
1365 .getPlayer(originalController) : originalController;
1366 if (this.controller != controller) {
1367
1368 MoveCard.moveCard(this, controller, getIdZone(idZone, tapped), null, 0,
1369 null, false);
1370 }
1371 ui.resetCachedData();
1372 }
1373
1374 @Override
1375 public int countAllCardsOf(Test test, Ability ability, boolean canBePreempted) {
1376 int result = 0;
1377 if (canBePreempted) {
1378 result = test.test(ability, this) ? 1 : 0;
1379 } else {
1380 result = test.testPreemption(ability, this) ? 1 : 0;
1381 }
1382 for (int j = getComponentCount(); j-- > 1;) {
1383 if (getComponent(j) instanceof MCard) {
1384 if (canBePreempted) {
1385 if (test.test(ability, (MCard) getComponent(j))) {
1386 result++;
1387 }
1388 } else if (!canBePreempted
1389 && test.testPreemption(ability, (MCard) getComponent(j))) {
1390 result++;
1391 }
1392 }
1393 }
1394 return result;
1395 }
1396
1397 @Override
1398 public void checkAllCardsOf(Test test, List<Target> list, Ability ability) {
1399 if (test.test(ability, this)) {
1400 list.add(this);
1401 }
1402 for (int j = getComponentCount(); j-- > 1;) {
1403 if (getComponent(j) instanceof MCard
1404 && test.test(ability, (MCard) getComponent(j))) {
1405 list.add((MCard) getComponent(j));
1406 }
1407 }
1408 }
1409
1410 /***
1411 * Indicates this card can be played from a specified zone.
1412 *
1413 * @param supposedZone
1414 * the zone where the this card would be played from.
1415 * @param idZone
1416 * the zone where this card can be played from.
1417 * @return true if this card is playable from the supposed zone
1418 */
1419 public boolean playableZone(int supposedZone, int idZone) {
1420 if (playableZoneModifier != null) {
1421 return playableZoneModifier.playableIn(idZone, idZone == supposedZone);
1422 }
1423 return supposedZone == idZone;
1424 }
1425
1426 /***
1427 * [un]Register ActivatedAbilities depending on the current zone of this card
1428 */
1429 public void updateAbilities() {
1430 for (Ability ability : cachedAbilities) {
1431 if (ability instanceof ActivatedAbility) {
1432 ability.removeFromManager();
1433 for (int j = IdZones.STACK; j-- > 0;) {
1434 if (ability.eventComing().isWellPlaced(j)) {
1435 ability.registerToManager();
1436 break;
1437 }
1438 }
1439 }
1440 }
1441 }
1442
1443 /***
1444 * Set to the register of this card a value to a specified index. The given
1445 * operation is uesed to apply operation on old and the given value. To set
1446 * the given value as the new one, use the "set" operation.
1447 *
1448 * @param index
1449 * is the index of register to modify
1450 * @param operation
1451 * the operation to use
1452 * @param rightValue
1453 * is the value to use as right operand for the operation
1454 */
1455 public void setValue(int index, Operation operation, int rightValue) {
1456 registers[index] = operation.process(registers[index], rightValue);
1457 if (index == IdCommonToken.DAMAGE) {
1458 MContextCardCardIntInt context = null;
1459 if (StackManager.getInstance().getAbilityContext() != null
1460 && StackManager.triggered.getAbilityContext() instanceof MContextCardCardIntInt
1461 && operation instanceof Add) {
1462 context = (MContextCardCardIntInt) StackManager.triggered
1463 .getAbilityContext();
1464 if (context.getValue() != rightValue) {
1465 throw new InternalError("wrong damage value regValue=" + rightValue
1466 + ", context.int=" + context.getValue());
1467 }
1468 }
1469 if (operation instanceof Add) {
1470 Log.debug(new StringBuilder("Add ").append(rightValue).append(
1471 " damage to card ").append(getCardName()).append("@").append(
1472 hashCode()).append(":").append(rightValue));
1473 Damage damage = new Damage(context == null ? SystemCard.instance
1474 : context.getCard2(), rightValue, context == null ? 0 : context
1475 .getValue2());
1476 damage.tap(tapped);
1477 add(damage);
1478 } else {
1479
1480 clearDamages();
1481 }
1482 ui.updateLayout();
1483 }
1484 if (index < IdTokens.FIRST_FREE_CARD_INDEX) {
1485
1486 int modifiedRegister = registerModifiers[index] != null ? registerModifiers[index]
1487 .getValue(getValueIndirection(index))
1488 : getValueIndirection(index);
1489 if (this.cachedRegisters[index] != modifiedRegister) {
1490
1491
1492 cachedRegisters[index] = modifiedRegister < 0 ? 0 : modifiedRegister;
1493 ui.resetCachedData();
1494
1495 }
1496 } else {
1497 repaint();
1498 }
1499 }
1500
1501 /***
1502 * Set a new zone for this card.
1503 *
1504 * @param idZone
1505 * the new zone.
1506 */
1507 public void setIdZone(int idZone) {
1508 this.idZone = idZone;
1509 }
1510
1511 @Override
1512 public void addTimestampReference() {
1513 timestampReferences++;
1514 }
1515
1516 @Override
1517 public void decrementTimestampReference(int timestamp) {
1518 if (this.timeStamp == timestamp) {
1519 timestampReferences--;
1520 } else if (lastKnownInstances.get(timestamp) != null
1521 && lastKnownInstances.get(timestamp).removeTimestamp(timestamp)) {
1522
1523
1524
1525
1526 lastKnownInstances.remove(timestamp);
1527 }
1528 }
1529
1530 /***
1531 * Return all properties of this card
1532 *
1533 * @return all properties of this card
1534 */
1535 public java.util.Set<Integer> getProperties() {
1536 return cachedProperties;
1537 }
1538
1539 @Override
1540 public Target getLastKnownTargetable(int timeStamp) {
1541 if (timeStamp == this.timeStamp) {
1542 assert timestampReferences > 0;
1543 return this;
1544 }
1545 if (lastKnownInstances.get(timeStamp) == null) {
1546 return this;
1547 }
1548 return lastKnownInstances.get(timeStamp).createLastKnownCard();
1549 }
1550
1551 @Override
1552 public int getTimestamp() {
1553 return timeStamp;
1554 }
1555
1556 /***
1557 * Return occurrences number of the given object with the given name attached
1558 * to this card.
1559 *
1560 * @param objectName
1561 * the object's name to find within the register modifiers chain.
1562 * @param objectTest
1563 * The test applied on specific modifier to be removed.
1564 * @return occurrences number of the given object with the given name attached
1565 * to this card.
1566 */
1567 public int getNbObjects(String objectName, Test objectTest) {
1568 return ObjectFactory.getObjectModifierModel(objectName).getNbObject(this,
1569 objectTest);
1570 }
1571
1572 @Override
1573 public void clearDamages() {
1574 for (int i = getComponentCount(); i-- > 0;) {
1575 if (getComponent(i) instanceof Damage) {
1576 remove(getComponent(i));
1577 }
1578 registers[IdCommonToken.DAMAGE] = 0;
1579 cachedRegisters[IdCommonToken.DAMAGE] = 0;
1580 }
1581 }
1582
1583 /***
1584 * The database configuration of this card
1585 */
1586 private DatabaseCard originalDatabase;
1587
1588 /***
1589 * The modified id card.
1590 */
1591 public int cachedIdCard;
1592
1593 /***