All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.puzzle.client.PuzzleController Maven / Gradle / Ivy

The newest version!
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/vilya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.puzzle.client;

import java.util.List;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import com.google.common.collect.Lists;

import com.samskivert.util.CollectionUtil;
import com.samskivert.util.ObserverList;

import com.samskivert.swing.util.MouseHijacker;

import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.ElementUpdateListener;
import com.threerings.presents.dobj.ElementUpdatedEvent;
import com.threerings.presents.dobj.NamedAttributeListener;

import com.threerings.crowd.client.PlaceControllerDelegate;
import com.threerings.crowd.data.PlaceObject;

import com.threerings.media.FrameParticipant;

import com.threerings.parlor.game.client.GameController;
import com.threerings.parlor.game.data.GameObject;

import com.threerings.puzzle.data.Board;
import com.threerings.puzzle.data.PuzzleCodes;
import com.threerings.puzzle.data.PuzzleObject;
import com.threerings.puzzle.util.PuzzleContext;

import static com.threerings.puzzle.Log.log;

/**
 * The puzzle game controller handles logical actions for a puzzle game.
 */
public abstract class PuzzleController extends GameController
    implements PuzzleCodes
{
    /** The action command to toggle chatting mode. */
    public static final String TOGGLE_CHATTING = "toggle_chat";

    /** Used by {@link PuzzleController#fireWhenActionCleared}. */
    public static interface ClearPender
    {
        /** {@link #actionCleared} return code. */
        public static final int RESTART_ACTION = -1;

        /** {@link #actionCleared} return code. */
        public static final int CARE_NOT = 0;

        /** {@link #actionCleared} return code. */
        public static final int NO_RESTART_ACTION = 1;

        /**
         * Called when the action is fully cleared.
         *
         * @return One of {@link #RESTART_ACTION}, {@link #CARE_NOT} or
         * {@link #NO_RESTART_ACTION}.
         */
        public int actionCleared ();
    }

    @Override
    protected void didInit ()
    {
        super.didInit();

        _panel = (PuzzlePanel)_view;
        _pctx = (PuzzleContext)_ctx;

        // initialize the puzzle panel
        _panel.init(_config);

        // initialize the board view
        _pview = _panel.getBoardView();
        _pview.setController(this);
    }

    /**
     * Creates and returns a new board model.
     */
    protected abstract Board newBoard ();

    /**
     * Returns the board associated with the puzzle.
     */
    public Board getBoard ()
    {
        return _pboard;
    }

    /**
     * Returns the player's index in the list of players for the game.
     */
    public int getPlayerIndex ()
    {
        return _pidx;
    }

    @Override
    public void setGameOver (boolean gameOver)
    {
        super.setGameOver(gameOver);

        // clear the action if we're informed that the game is over early by the client
        if (gameOver) {
            clearAction();
        }
    }

    /**
     * Returns true if the puzzle has action, false if the action is cleared or it is suspended.
     */
    public boolean hasAction ()
    {
        return (_astate == ACTION_GOING);
    }

    /**
     * Sets whether we're focusing on the chat window rather than the puzzle.
     */
    public void setChatting (boolean chatting)
    {
        // ignore the request if we're already there
        if ((isChatting() == chatting) ||
            // ..or if we want to initiate chatting and..
            // we either can't right now or we don't have action
            (chatting && (!canStartChatting() || !hasAction()))) {
            return;
        }

        // update the panel
        _panel.setPuzzleGrabsKeys(!chatting);

        // if we're moving focus to chat..
        if (chatting) {
            if (_unpauser != null) {
                log.warning("Huh? Already have a mouse unpauser?");
                _unpauser.release();
            }
            _unpauser = new Unpauser(_panel);

        } else {
            if (_unpauser != null) {
                _unpauser.release();
                _unpauser = null;
            }
        }

        // update the chatting state
        _chatting = chatting;

        // dispatch the change to our delegates
        applyToDelegates(new DelegateOp(PuzzleControllerDelegate.class) {
            @Override
            public void apply (PlaceControllerDelegate delegate) {
                ((PuzzleControllerDelegate)delegate).setChatting(_chatting);
            }
        });

        // and check if we should be suspending the action during this pause
        if (supportsActionPause()) {
            // clear the action if we're pausing, resume it if we're unpausing
            if (chatting) {
                clearAction();
            } else {
                safeStartAction();
            }
            _pview.setPaused(chatting);
        }
    }

    /**
     * Get the (untranslated) string to display when the puzzle is paused.
     */
    public String getPauseString ()
    {
        return "m.paused";
    }

    /**
     * Derived classes should override this and return false if their action should not be paused
     * when the user switches control to the chat area.
     */
    protected boolean supportsActionPause ()
    {
        return true;
    }

    /**
     * Can we start chatting at this juncture?
     */
    protected boolean canStartChatting ()
    {
        // check with the delegates
        final boolean[] canChatNow = new boolean[] { true };
        applyToDelegates(new DelegateOp(PuzzleControllerDelegate.class) {
            @Override
            public void apply (PlaceControllerDelegate delegate) {
                canChatNow[0] =
                    ((PuzzleControllerDelegate)delegate).canStartChatting() && canChatNow[0];
            }
        });
        return canChatNow[0];
    }

    /**
     * Returns true if the puzzle has been defocused because the player is doing some chatting.
     */
    public boolean isChatting ()
    {
        return _chatting;
    }

    @Override
    public void willEnterPlace (PlaceObject plobj)
    {
        super.willEnterPlace(plobj);

        // get a casted reference to our puzzle object
        _puzobj = (PuzzleObject)plobj;
        _puzobj.addListener(_kolist);
        _puzobj.addListener(_mlist);

        // listen to key events..
        _pctx.getKeyDispatcher().addGlobalKeyListener(_globalKeyListener);

        // save off our player index
        _pidx = _puzobj.getPlayerIndex(_pctx.getUsername());

        // generate the starting board
        generateNewBoard();

        // if the game is already in play, start up the action
        if (_puzobj.isInPlay() && _puzobj.isActivePlayer(_pidx)) {
            startAction();
        }
    }

    @Override
    public void mayLeavePlace (PlaceObject plobj)
    {
        super.mayLeavePlace(plobj);

        // flush any pending progress events
        sendProgressUpdate();
    }

    @Override
    public void didLeavePlace (PlaceObject plobj)
    {
        super.didLeavePlace(plobj);

        // clean up and clear out
        clearAction();

        // stop listening to key events..
        _pctx.getKeyDispatcher().removeGlobalKeyListener(_globalKeyListener);

        // clear out the puzzle object
        if (_puzobj != null) {
            _puzobj.removeListener(_mlist);
            _puzobj.removeListener(_kolist);
            _puzobj = null;
        }
    }

    /**
     * Puzzles that do not have "action" that starts and stops (via {@link #startAction} and
     * {@link #clearAction}) when the puzzle starts and stops can override this method and return
     * false.
     */
    protected boolean isActionPuzzle ()
    {
        return true;
    }

    /**
     * Indicates whether the action should start immediately as a result of
     * {@link GameController#gameDidStart} being called. If a puzzle wishes to do some beginning
     * of the game fun stuff, like display a tutorial screen, they can veto the action start and
     * then start it themselves later.
     */
    protected boolean startActionImmediately ()
    {
        return true;
    }

    @Override
    public void attributeChanged (AttributeChangedEvent event)
    {
        String name = event.getName();

        // deal with game state changes
        if (name.equals(PuzzleObject.STATE)) {
            switch (event.getIntValue()) {
            case PuzzleObject.IN_PLAY:
                // we have to postpone all game starting activity until the current action has
                // ended; only after all the animations have been completed will everything be in a
                // state fit for starting back up again
                fireWhenActionCleared(new ClearPender() {
                    public int actionCleared () {
                        // do the standard game did start business
                        gameDidStart();
                        // we don't always start the action immediately
                        return startActionImmediately() ? RESTART_ACTION : CARE_NOT;
                    }
                });
                break;

            case PuzzleObject.GAME_OVER:
                // similarly we have to postpone game ending activity until the current action has
                // ended clean up and clear out
                clearAction();
                // wait until the action is cleared before we roll down to our delegates and do all
                // that business
                fireWhenActionCleared(new ClearPender() {
                    public int actionCleared () {
                        gameDidEnd();
                        return CARE_NOT;
                    }
                });
                break;

            default:
                super.attributeChanged(event);
                break;
            }

        } else if (name.equals(PuzzleObject.SESSION_ID)) {
            // Need to clear out stale events.  If we don't, we could send events that claim to be
            // from the new session that are actually from the old session.
            _events.clear();
        }
    }

    @Override
    protected void gameWillReset ()
    {
        super.gameWillReset();

        // stop the old action
        clearAction();

        // when the server gets around to resetting the game, we'll get a 'state => IN_PLAY'
        // message which will result in gameDidStart() being called and starting the action back up
    }

    /**
     * Called when a new board is set.
     */
    public void setBoard (Board board)
    {
        // we don't need to do anything by default
    }

    /**
     * Derived classes should override this method and do whatever is necessary to start up the
     * action for their puzzle. This could be called when the user is already in the "room" and
     * the game starts, or immediately upon entering the room if the game is already started (for
     * example if they disconnected and reconnected to a game already in progress).
     */
    protected void startAction ()
    {
        // do nothing if we're not an action puzzle
        if (!isActionPuzzle()) {
            return;
        }

        // refuse to start the action if our puzzle view is hidden
        if (_pidx != -1 && !_pview.isShowing()) {
            log.warning("Refusing to start action on hidden puzzle.", new Exception());
            return;
        }

        // refuse to start the action if it's already going
        if (_astate != ACTION_CLEARED) {
            log.warning("Action state inappropriate for startAction()", "astate", _astate,
                        new Exception());
            return;
        }

        if (isChatting() && supportsActionPause()) {
            log.info("Not starting action, player is chatting in a puzzle that supports " +
                     "pausing the action.");
            return;
        }

        log.debug("Starting puzzle action.");

        // register the game progress updater; it may already be updated because we can cycle
        // through clearing the action and starting it again before the updater gets a chance to
        // unregister itself
        if (!_pctx.getFrameManager().isRegisteredFrameParticipant(_updater)) {
            _pctx.getFrameManager().registerFrameParticipant(_updater);
        }

        // make a note that we've started the action
        _astate = ACTION_GOING;

        // let our panel know what's up
        _panel.startAction();

        // and if we're not currently chatting, set the puzzle to grab keys and for the chatbox to
        // look disabled
        if (!isChatting()) {
            _panel.setPuzzleGrabsKeys(true);
        }

        // let our delegates do their business
        applyToDelegates(new DelegateOp(PuzzleControllerDelegate.class) {
            @Override
            public void apply (PlaceControllerDelegate delegate) {
                ((PuzzleControllerDelegate)delegate).startAction();
            }
        });
    }

    /**
     * If it is not known whether the puzzle board view has finished animating its final bits
     * after a previous call to {@link #clearAction}, this method should be used instead of
     * {@link #startAction} as it will wait until the action is confirmedly over before starting
     * it anew.
     */
    protected void safeStartAction ()
    {
        // do nothing if we're not an action puzzle
        if (!isActionPuzzle()) {
            return;
        }

        fireWhenActionCleared(new ClearPender() {
            public int actionCleared () {
                return RESTART_ACTION;
            }
        });
    }

    /**
     * Called when the game has ended or when it is going to reset and when the client leaves the
     * game "room". This method does not always immediately clear the action, but may mark the
     * clear as pending if the action cannot yet be cleared (as indicated by
     * {@link #canClearAction}). The action will eventually be cleared which will result in a call
     * to {@link #actuallyClearAction} which is what derived classes should override to do their
     * action clearing business.
     */
    protected void clearAction ()
    {
        // do nothing if we're not an action puzzle
        if (!isActionPuzzle()) {
            return;
        }

        // no need to clear if we're already cleared or clearing
        if (_astate == CLEAR_PENDING || _astate == ACTION_CLEARED) {
            return;
        }

        log.debug("Attempting to clear puzzle action.");

        // put ourselves into a pending clear state and attempt to clear the action
        _astate = CLEAR_PENDING;
        maybeClearAction();
    }

    /**
     * This method is called by the {@link PuzzleBoardView} when all action on the board has
     * finished.
     */
    protected void boardActionCleared ()
    {
        // if we have a clear pending, this could be the trigger that allows us to clear our action
        maybeClearAction();
    }

    /**
     * Queues up code to be invoked when the action is completely cleared (including all remaining
     * interesting sprites and animations on the puzzle board).
     */
    protected void fireWhenActionCleared (ClearPender pender)
    {
        // if the action is already ended, fire this pender immediately
        if (_astate == ACTION_CLEARED) {
            if (pender.actionCleared() == ClearPender.RESTART_ACTION) {
                log.debug("Restarting action at behest of pender " + pender + ".");
                startAction();
            }

        } else {
            log.debug("Queueing action pender " + pender + ".");
            _clearPenders.add(pender);
        }
    }

    /**
     * Returns whether or not it is safe to clear the action. The default behavior is to not allow
     * the action to be cleared until all interesting sprites and animations in the board view
     * have finished. If derived classes or delegates wish to postpone the clearing of the action,
     * they can return false from this method, but they must then be sure to call
     * {@link #maybeClearAction} when whatever condition that caused them to desire to postpone
     * action clearing has finally been satisfied.
     */
    protected boolean canClearAction ()
    {
        final boolean[] canClear = new boolean[1];
        canClear[0] = (_pview.getActionCount() == 0);
//         if (!canClear[0]) {
//             _pview.dumpActors();
//             PuzzleBoardView.DEBUG_ACTION = true;
//         }

        // let our delegates do their business
        applyToDelegates(new DelegateOp(PuzzleControllerDelegate.class) {
            @Override
            public void apply (PlaceControllerDelegate delegate) {
                canClear[0] = canClear[0] && ((PuzzleControllerDelegate)delegate).canClearAction();
            }
        });

        return canClear[0];
    }

    /**
     * Called to effect the actual clearing of our action if we've received some asynchronous
     * trigger that indicates that it may well be safe now to clear the action.
     */
    protected void maybeClearAction ()
    {
        if (_astate == CLEAR_PENDING && canClearAction()) {
            actuallyClearAction();
//        } else {
//            log.info("Not clearing action", "astate", _astate, "canClear", canClearAction());
        }
    }

    /**
     * Performs the actual process of clearing the action for this puzzle. This is only called
     * after it is known to be safe to clear the action. Derived classes can override this method
     * and clear out anything that is not needed while the puzzle's "action" is not going (timers,
     * etc.). Anything that is cleared out here should be recreated in {@link #startAction}.
     */
    protected void actuallyClearAction ()
    {
        log.debug("Actually clearing action.");

        // make a note that we've cleared the action
        _astate = ACTION_CLEARED;
//         PuzzleBoardView.DEBUG_ACTION = false;

        // let our delegates do their business
        applyToDelegates(new DelegateOp(PuzzleControllerDelegate.class) {
            @Override
            public void apply (PlaceControllerDelegate delegate) {
                ((PuzzleControllerDelegate)delegate).clearAction();
            }
        });

        // let our panel know what's up
        _panel.clearAction();
        _panel.setPuzzleGrabsKeys(false); // let the user chat

        // deliver one final update to the server
        sendProgressUpdate();

        // let derived classes do things
        try {
            actionWasCleared();
        } catch (Exception e) {
            log.warning("Choked in actionWasCleared", e);
        }

        // notify any penders that the action has cleared
        final int[] results = new int[2];
        _clearPenders.apply(new ObserverList.ObserverOp() {
            public boolean apply (ClearPender observer) {
                switch (observer.actionCleared()) {
                case ClearPender.RESTART_ACTION: results[0]++; break;
                case ClearPender.NO_RESTART_ACTION: results[1]++; break;
                }
                return true;
            }
        });
        _clearPenders.clear();

        // if there are no refusals and at least one restart request, go ahead and restart the
        // action now
        if (results[1] == 0 && results[0] > 0) {
            startAction();
        }
    }

    /**
     * Called when the action was actually cleared, but before the action observers are notified.
     */
    protected void actionWasCleared ()
    {
    }

    @Override
    public boolean handleAction (ActionEvent action)
    {
        String cmd = action.getActionCommand();
        if (cmd.equals(TOGGLE_CHATTING)) {
            setChatting(!isChatting());

        } else {
            return super.handleAction(action);
         }

        return true;
    }

    /**
     * Returns the delay in milliseconds between sending each progress update event to the server.
     * Derived classes may wish to override this to send their progress updates more or less
     * frequently than the default.
     */
    protected long getProgressInterval ()
    {
        return DEFAULT_PROGRESS_INTERVAL;
    }

    /**
     * Signal the game to generate and distribute a new board.
     */
    protected void generateNewBoard ()
    {
        // wait for any animations or sprites in the board to finish their business before setting
        // the board into place
        fireWhenActionCleared(new ClearPender() {
            public int actionCleared () {
                // update the player board
                _pboard = newBoard();
                if (_puzobj.seed != 0) {
                    _pboard.initializeSeed(_puzobj.seed);
                }
                setBoard(_pboard);
                _pview.setBoard(_pboard);

                // and repaint things
                _pview.repaint();

                // let our delegates do their business
                applyToDelegates(new DelegateOp(PuzzleControllerDelegate.class) {
                    @Override
                    public void apply (PlaceControllerDelegate delegate) {
                        ((PuzzleControllerDelegate)delegate).setBoard(_pboard);
                    }
                });

                return CARE_NOT;
            }
        });
    }

    /**
     * Returns the number of progress events currently queued up for sending to the server with
     * the next progress update.
     */
    public int getEventCount ()
    {
        return _events.size();
    }

    /**
     * Are we syncing boards for this puzzle? By default, we defer to the PuzzlePanel and its
     * runtime config.
     */
    protected boolean isSyncingBoards ()
    {
        return PuzzlePanel.isSyncingBoards();
    }

    /**
     * Adds the given progress event and a snapshot of the supplied board state to the set of
     * progress events and associated board states for later transmission to the server.
     */
    public void addProgressEvent (int event, Board board)
    {
        // make sure they don't queue things up at strange times
        if (_puzobj.state != PuzzleObject.IN_PLAY) {
            log.warning("Rejecting progress event; game not in play",
                "puzobj", _puzobj.which(), "event", event);
            return;
        }

        getBoard().seedFromEvent(getPlayerIndex(), event);

        _events.add(Integer.valueOf(event));
        if (isSyncingBoards()) {
            _states.add((board == null) ? null : board.clone());
            if (board == null) {
                log.warning(
                    "Added progress event with no associated board state, " +
                    "server will not be able to ensure board state synchronization.");
            }
        }
    }

    /**
     * Sends the server a game progress update with the list of events, as well as board states if
     * {@link PuzzlePanel#isSyncingBoards} is true.
     */
    public void sendProgressUpdate ()
    {
        // make sure we have our puzzle object and events to send
        int size = _events.size();
        if (size == 0 || _puzobj == null) {
            return;
        }

        // create an array of the events we're sending to the server
        int[] events = CollectionUtil.toIntArray(_events);
        _events.clear();

//        log.info("Sending progress", "session", _puzobj.sessionId,
//            "events", StringUtil.toString(events));

        // create an array of the board states that correspond with those events (if state syncing
        // is enabled)
        int numStates = _states.size();
        if (numStates == size) { // ie, if we have a board to match every event
            Board[] states = new Board[numStates];
            _states.toArray(states);
            _states.clear();

            // send the update progress request
            _puzobj.puzzleGameService.updateProgressSync(_puzobj.sessionId, events, states);

        } else {
            // send the update progress request
            _puzobj.puzzleGameService.updateProgress(_puzobj.sessionId, events);
        }
    }

    /**
     * Called when a player is knocked out of the game to give the puzzle a chance to perform any
     * post-knockout actions that may be desired. Derived classes may wish to override this method
     * but should be sure to call super.playerKnockedOut().
     */
    protected void playerKnockedOut (final int pidx)
    {
        // dispatch this to our delegates
        applyToDelegates(new DelegateOp(PuzzleControllerDelegate.class) {
            @Override
            public void apply (PlaceControllerDelegate delegate) {
                ((PuzzleControllerDelegate)delegate).playerKnockedOut(pidx);
            }
        });
    }

    /**
     * Catches clicks an unpauses, without passing the click through to the puzzle.
     */
    class Unpauser extends MouseHijacker
    {
        public Unpauser (PuzzlePanel panel) {
            super(panel.getBoardView());
            _panel = panel;
            panel.addMouseListener(_clicker);
            panel.getBoardView().addMouseListener(_clicker);
        }

        @Override
        public Component release () {
            _panel.removeMouseListener(_clicker);
            _panel.getBoardView().removeMouseListener(_clicker);
            return super.release();
        }

        protected MouseAdapter _clicker = new MouseAdapter() {
            @Override
            public void mousePressed (MouseEvent event) {
                setChatting(false); // this will call release
            }
        };

        protected PuzzlePanel _panel;
    }

    /**
     * A special frame participant that handles the sending of puzzle progress updates. We can't
     * just register an interval for this because sometimes the clock goes backwards in time in
     * windows and our intervals don't get called for a long period of time which causes the
     * server to think the client is disconnected or cheating and resign them from the puzzle. God
     * bless you, Microsoft.
     */
    protected class Updater implements FrameParticipant
    {
        public void tick (long tickStamp) {
            if (_astate == ACTION_CLEARED) {
                // remove ourselves as the action is now cleared; we can't do this in
                // actuallyClearAction() because that might get called during the PuzzlePanel's
                // frame tick and it's only safe to remove yourself during a tick(), not another
                // frame participant
                _pctx.getFrameManager().removeFrameParticipant(_updater);

            } else if (tickStamp - _lastProgressTick > getProgressInterval()) {
                _lastProgressTick = tickStamp;
                sendProgressUpdate();
            }
        }

        public boolean needsPaint () {
            return false;
        }

        public Component getComponent () {
            return null;
        }

        public long _lastProgressTick;
    }

    /**
     * Create the updater to be used in this puzzle.
     */
    protected Updater createUpdater ()
    {
        return new Updater();
    }

    /** The mouse jockey for unpausing our puzzles. */
    protected Unpauser _unpauser;

    /** Handles the sending of puzzle progress updates. */
    protected Updater _updater = createUpdater();

    /** Listens for players being knocked out. */
    protected ElementUpdateListener _kolist = new ElementUpdateListener() {
        public void elementUpdated (ElementUpdatedEvent event) {
            String name = event.getName();
            if (name.equals(PuzzleObject.PLAYER_STATUS)) {
                if (event.getIntValue() == GameObject.PLAYER_LEFT_GAME) {
                    playerKnockedOut(event.getIndex());
                }
            }
        }
    };

    /** Listens for various attribute changes. */
    protected AttributeChangeListener _mlist = new NamedAttributeListener(PuzzleObject.SEED) {
        @Override
        public void namedAttributeChanged (AttributeChangedEvent event) {
            generateNewBoard();
        }
    };

    /** A casted reference to the client context. */
    protected PuzzleContext _pctx;

    /** Our player index in the game. */
    protected int _pidx;

    /** The puzzle panel. */
    protected PuzzlePanel _panel;

    /** A reference to our puzzle game object. */
    protected PuzzleObject _puzobj;

    /** The puzzle board view. */
    protected PuzzleBoardView _pview;

    /** The puzzle board data. */
    protected Board _pboard;

    /** The list of relevant game events since the last progress update. */
    protected List _events = Lists.newArrayList();

    /** Board snapshots that correspond to board state after each event has been applied. */
    protected List _states = Lists.newArrayList();

    /** A flag indicating that we're in chatting mode. */
    protected boolean _chatting = false;

    /** The current action state of the puzzle. */
    protected int _astate = ACTION_CLEARED;

    /** The action cleared penders. */
    protected ObserverList _clearPenders = ObserverList.newSafeInOrder();

    /** A key listener that currently just toggles pause in the puzzle. */
    protected KeyListener _globalKeyListener = new KeyAdapter() {
        @Override
        public void keyReleased (KeyEvent e) {
            int keycode = e.getKeyCode();
            // toggle chatting (pause)
            if (keycode == KeyEvent.VK_ESCAPE || keycode == KeyEvent.VK_PAUSE) {
                setChatting(!isChatting());

            // pressing P also to pause (but not unpause), and only if it has not been reassigned
            } else if (keycode == KeyEvent.VK_P && !isChatting() &&
                    !_panel._xlate.hasCommand(KeyEvent.VK_P)) {
                setChatting(true);
            }
        }
    };

    /** The delay in milliseconds between progress update intervals. */
    protected static final long DEFAULT_PROGRESS_INTERVAL = 6000L;

    /** A {@link #_astate} constant. */
    protected static final int ACTION_CLEARED = 0;

    /** A {@link #_astate} constant. */
    protected static final int CLEAR_PENDING = 1;

    /** A {@link #_astate} constant. */
    protected static final int ACTION_GOING = 2;
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy