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

com.threerings.chat.ChatLogic Maven / Gradle / Ivy

The newest version!
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// https://github.com/threerings/nenya
//
// 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.chat;

import java.awt.Color;
import java.awt.Font;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;

import com.samskivert.swing.Label;
import com.samskivert.util.Tuple;

import com.threerings.crowd.chat.data.ChatCodes;

/**
 * Contains all of the routines that might (or must) be customized by a system that makes use of
 * the comic chat system.
 */
public abstract class ChatLogic
{
    /** The default chat decay parameter. See {@link #getDisplayDurationIndex}. */
    public static final int DEFAULT_CHAT_DECAY = 1;

    /** The padding in each direction around the text to the edges of a chat 'bubble'. */
    public static final int PAD = 10;

    /** Type mode code for default chat type (speaking). */
    public static final int SPEAK = 0;

    /** Type mode code for shout chat type. */
    public static final int SHOUT = 1;

    /** Type mode code for emote chat type. */
    public static final int EMOTE = 2;

    /** Type mode code for think chat type. */
    public static final int THINK = 3;

    /** Type place code for default place chat (cluster, scene). */
    public static final int PLACE = 1 << 4;

    // 2 and 3 are skipped for legacy migration reasons

    /** Type code for a chat type that was used in some special context, like in a negotiation. */
    public static final int SPECIALIZED = 4 << 4;

    /** Our internal code for tell chat. */
    public static final int TELL = 5 << 4;

    /** Our internal code for tell feedback chat. */
    public static final int TELLFEEDBACK = 6 << 4;

    /** Our internal code for info system messges. */
    public static final int INFO = 7 << 4;

    /** Our internal code for feedback system messages. */
    public static final int FEEDBACK = 8 << 4;

    /** Our internal code for attention system messages. */
    public static final int ATTENTION = 9 << 4;

    /** Our internal code for any type of chat that is continued in a subtitle. */
    public static final int CONTINUATION = 10 << 4;

    /** Type place code for broadcast chat type. */
    public static final int BROADCAST = 11 << 4;

    /** Our internal code for a chat type we will ignore. */
    public static final int IGNORECHAT = -1;

    /**
     * Returns the message bundle used to translate default messages.
     */
    public abstract String getDefaultMessageBundle ();

    /**
     * Determines the format string and whether to use quotes based on the chat type.
     */
    public Tuple decodeFormat (int type, String format)
    {
        boolean quotes = true;
        switch (placeOf(type)) {
        // derived classes may wish to change the format here based on the place
        case PLACE:
            switch (modeOf(type)) {
            case EMOTE:
                quotes = false;
                break;
            }
            break;
        }
        return Tuple.newTuple(format, quotes);
    }

    /**
     * Decodes the main chat type given the supplied localtype provided by the chat system.
     */
    public int decodeType (String localtype)
    {
        if (ChatCodes.USER_CHAT_TYPE.equals(localtype)) {
            return TELL;
        } else if (ChatCodes.PLACE_CHAT_TYPE.equals(localtype)) {
            return PLACE;
        } else {
            return 0;
        }
    }

    /**
     * Adjust the chat type based on the mode of the chat message.
     */
    public int adjustTypeByMode (int mode, int type)
    {
        switch (mode) {
        case ChatCodes.DEFAULT_MODE:
            return type | SPEAK;
        case ChatCodes.EMOTE_MODE:
            return type | EMOTE;
        case ChatCodes.THINK_MODE:
            return type | THINK;
        case ChatCodes.SHOUT_MODE:
            return type | SHOUT;
        case ChatCodes.BROADCAST_MODE:
            return BROADCAST; // broadcast always looks like broadcast
        default:
            return type;
        }
    }

    /**
     * Get the font to use for the given bubble type.
     */
    public Font getFont (int type)
    {
        return DEFAULT_FONT;
    }

    /**
     * Creates a label for the specified text. Derived classes may wish to use specialized labels.
     */
    public Label createLabel (String text)
    {
        return new Label(text);
    }

    /**
     * Computes the chat glyph outline color from the chat message type.
     */
    public Color getOutlineColor (int type)
    {
        switch (type) {
        case BROADCAST:
            return BROADCAST_COLOR;
        case TELL:
            return TELL_COLOR;
        case TELLFEEDBACK:
            return TELLFEEDBACK_COLOR;
        case INFO:
            return INFO_COLOR;
        case FEEDBACK:
            return FEEDBACK_COLOR;
        case ATTENTION:
            return ATTENTION_COLOR;
        default:
            return Color.black;
        }
    }

    /**
     * Get the appropriate shape for the specified type of chat.
     *
     * @param r the rectangle bounding the chat label.
     * @param b the rectangle bounding the chat label and icon.
     */
    public Shape getSubtitleShape (int type, Rectangle r, Rectangle b)
    {
        int placeType = placeOf(type);
        switch (placeType) {
        case SPECIALIZED:
        case PLACE:
            return getPlaceSubtitleShape(type, r);

        case TELL: {
            // lightning box!
            int halfy = r.y + r.height/2;
            Polygon p = new Polygon();
            p.addPoint(r.x - PAD - 2, r.y);
            p.addPoint(r.x - PAD / 2 - 2, halfy);
            p.addPoint(r.x - PAD, halfy);
            p.addPoint(r.x - PAD / 2, r.y + r.height);
            p.addPoint(r.x + r.width + PAD + 2, r.y + r.height);
            p.addPoint(r.x + r.width + PAD / 2 + 2, halfy);
            p.addPoint(r.x + r.width + PAD, halfy);
            p.addPoint(r.x + r.width + PAD / 2, r.y);
            return p;
        }

        case TELLFEEDBACK: {
            // reverse-lightning box!
            int halfy = r.y + r.height/2;
            Polygon p = new Polygon();
            p.addPoint(r.x - PAD - 2, r.y + r.height);
            p.addPoint(r.x - PAD / 2 - 2, halfy);
            p.addPoint(r.x - PAD, halfy);
            p.addPoint(r.x - PAD / 2, r.y);
            p.addPoint(r.x + r.width + PAD + 2, r.y);
            p.addPoint(r.x + r.width + PAD / 2 + 2, halfy);
            p.addPoint(r.x + r.width + PAD, halfy);
            p.addPoint(r.x + r.width + PAD / 2, r.y + r.height);
            return p;
        }

        case FEEDBACK: {
            // slanted box subtitle
            Polygon p = new Polygon();
            p.addPoint(r.x - PAD / 2, r.y);
            p.addPoint(r.x + r.width + PAD, r.y);
            p.addPoint(r.x + r.width + PAD / 2, r.y + r.height);
            p.addPoint(r.x - PAD, r.y + r.height);
            return p;
        }

        case BROADCAST:
        case CONTINUATION:
        case INFO:
        case ATTENTION:
        default: {
            Rectangle grown = new Rectangle(r);
            grown.grow(PAD, 0);
            return new Area(grown);
        }
        }
    }

    /**
     * Get the spacing, in pixels, between the latest subtitle of the specified type and the
     * previous subtitle.
     */
    public int getSubtitleSpacing (int type)
    {
        switch (placeOf(type)) {
        // derived classes may wish to adjust subtitle spacing here based on chat type
        default:
            return 1;
        }
    }

    /**
     * A helper function for {@link #getSubtitleShape}.
     */
    public Shape getPlaceSubtitleShape (int type, Rectangle r)
    {
        switch (modeOf(type)) {
        default:
        case SPEAK: {
            // rounded rectangle subtitle
            Area a = new Area(r);
            a.add(new Area(new Ellipse2D.Float(r.x - PAD, r.y, PAD * 2, r.height)));
            a.add(new Area(new Ellipse2D.Float(r.x + r.width - PAD, r.y, PAD * 2, r.height)));
            return a;
        }

        case THINK: {
            r.grow(PAD / 2, 0);
            Area a = new Area(r);
            int dia = 8;
            int num = (int) Math.ceil(r.height / ((float) dia));
            int leftside = r.x - dia/2;
            int rightside = r.x + r.width - dia/2 - 1;

            int maxh = r.height - dia;
            for (int ii=0; ii < num; ii++) {
                int ypos = r.y + Math.min((r.height * ii) / num, maxh);
                a.add(new Area(new Ellipse2D.Float(leftside, ypos, dia, dia)));
                a.add(new Area(new Ellipse2D.Float(rightside, ypos, dia, dia)));
            }
            return a;
        }

        case SHOUT: {
            r.grow(PAD / 2, 0);
            Area a = new Area(r);
            Polygon left = new Polygon();
            Polygon right = new Polygon();

            int spikehei = 8;
            int num = (int) Math.ceil(r.height / ((float) spikehei));
            left.addPoint(r.x, r.y);
            left.addPoint(r.x - PAD, r.y + spikehei / 2);
            left.addPoint(r.x, r.y + spikehei);
            right.addPoint(r.x + r.width , r.y);
            right.addPoint(r.x + r.width + PAD, r.y + spikehei / 2);
            right.addPoint(r.x + r.width, r.y + spikehei);

            int ypos = 0;
            int maxpos = r.y + r.height - spikehei + 1;
            for (int ii=0; ii < num; ii++) {
                int newpos = Math.min((r.height * ii) / num, maxpos);
                left.translate(0, newpos - ypos);
                right.translate(0, newpos - ypos);
                a.add(new Area(left));
                a.add(new Area(right));
                ypos = newpos;
            }

            return a;
        }

        case EMOTE: {
            // a box that curves inward on the left and right
            r.grow(PAD, 0);
            Area a = new Area(r);
            a.subtract(new Area(new Ellipse2D.Float(r.x - PAD / 2, r.y, PAD, r.height)));
            a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - PAD / 2, r.y, PAD, r.height)));
            return a;
        }
        }
    }

    /**
     * Returns metrics on how long chat messages should be displayed.
     */
    public long[] getDisplayDurations (int indexOffset)
    {
        return DISPLAY_DURATION_PARAMS[getDisplayDurationIndex() + indexOffset];
    }

    /**
     * Get the current display duration parameters: 0 = fast, 1 = medium, 2 = long.
     * See {@link #DISPLAY_DURATION_PARAMS}.
     */
    protected int getDisplayDurationIndex ()
    {
        return DEFAULT_CHAT_DECAY;
    }

    /**
     * Extracts the mode constant from the type value.
     */
    protected static int modeOf (int type)
    {
        return type & 0xF;
    }

    /**
     * Extract the place constant from the type value.
     */
    protected static int placeOf (int type)
    {
        return type & ~0xF;
    }

    /**
     * Times to display chat: { (time per character), (min time), (max time) }
     *
     * Groups 0/1/2 are short/medium/long for chat bubbles, and groups 1/2/3 are short/medium/long
     * for subtitles.
     */
    protected static final long[][] DISPLAY_DURATION_PARAMS = {
        { 125L, 10000L, 30000L },  // fastest durations
        { 200L, 15000L, 40000L },  // medium (default) durations
        { 275L, 20000L, 50000L },  // longest regular duration..
        { 350L, 25000L, 60000L }   // grampatime!
    };

    // used to color chat bubbles
    protected static final Color BROADCAST_COLOR = new Color(0x990000);
    protected static final Color FEEDBACK_COLOR = new Color(0x00AA00);
    protected static final Color TELL_COLOR = new Color(0x0000AA);
    protected static final Color TELLFEEDBACK_COLOR = new Color(0x00AAAA);
    protected static final Color INFO_COLOR = new Color(0xAAAA00);
    protected static final Color ATTENTION_COLOR = new Color(0xFF5000);

    /** Our default chat font. */
    protected static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 12);
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy