/**
* A two player game by Jimmy Andrews and Loren Schmidt. Hold the buttons (A Z UP DOWN) to start!
*
* (For keyboards where A+Z is uncomfortable, you can alternatively use S+X)
*/
/* // this html code might help avoid anti-aliasing if we put it on the webpage ...
canvas {
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: optimize-contrast;
-ms-interpolation-mode: nearest-neighbor;
}
*/
KissStage stage;
TitleScreen title;
ConsentPhase consent;
EndScreen endscreen;
PFont f;
PGraphics pg;
boolean useBuffer = false; // if true, we render all our graphics to a buffer and the upscale; otherwise we render direct to canvas
int STATE_TITLE = 0, STATE_CONSENT = 1, STATE_KISS = 2, STATE_END = 3, NUM_STATES = 4;
int gameState = STATE_TITLE;
int lastGameState = gameState;
double transition = 0;
PVector playerColor[] = {new PVector(174,0,255),new PVector(38,119,136)};
void setup ()
{
size( 640, 480 );
stage = new KissStage();
title = new TitleScreen();
consent = new ConsentPhase();
endscreen = new EndScreen();
f = createFont("Georgia", 34);
textFont(f);
frameRate(30);
pg = createGraphics(320, 240);
}
void draw()
{
background(0); stroke(255); fill(255);
if (transition == 0 && gameState != lastGameState) { // begin a state transition
transition = .01;
}
if (transition > 0) {
if (transitionScreen()) {
return;
}
}
lastGameState = gameState;
background(0); stroke(255); fill(255);
drawStateWithUpdate(gameState);
}
void drawTitle() {
title.update();
title.render();
}
void drawConsent() {
consent.update();
consent.render();
}
void drawKiss() {
stage.draw();
stage.step();
}
void drawEnd() {
endscreen.update();
endscreen.render();
}
void resetPhase(int state) {
if (state == STATE_TITLE) {
title = new TitleScreen();
} else if (state == STATE_CONSENT) {
consent = new ConsentPhase();
} else if (state == STATE_KISS) {
stage = new KissStage();
}
}
void drawStateWithUpdate(int state) {
if (state == STATE_TITLE) {
drawTitle();
} else if (state == STATE_CONSENT) {
drawConsent();
} else if (state == STATE_KISS) {
drawKiss();
} else if (state == STATE_END) {
drawEnd();
}
}
void drawStateNoUpdate(int state) {
if (state == STATE_TITLE) {
title.render();
} else if (state == STATE_CONSENT) {
consent.render();
} else if (state == STATE_KISS) {
stage.draw();
} else if (state == STATE_END) {
endscreen.render();
}
}
void heart(float x, float y, float w, float h) {
ellipse(x-w/2+w*.05, y-h/4, w, w);//, PI, 2*PI);
ellipse(x+w/2-w*.05, y-h/4, w, w);//, PI, 2*PI);
//fill(50);
quad(x-w*.81,y+.1*h,x,y-h/4,x+w*.81,y+.1*h,x,y+h);
}
// return false if the transition is done
boolean transitionScreen() {
transition += .05;
float scaleF = 1.2;
if (transition > 1) {
double t = (transition-1);
drawStateWithUpdate(gameState);
noStroke();
fill(195,58,180);
heart(width/2,height/2+(1-t)*height*.2,scaleF*width*(1-t),scaleF*width*(1-t));
} else {
double t = transition;
drawStateNoUpdate(lastGameState);
noStroke();
fill(195,58,180);
heart(width/2,height/2+height*.2*t,scaleF*width*t,scaleF*width*t);
}
if (transition > 2) {
transition = 0;
resetPhase(lastGameState);
return false;
} else {
return true;
}
}
/* @pjs preload="kissRequestPrompt.png, p1KissRequestText.png, p1KissResponseNo.png, p1KissResponseYes.png, p2ResponsePrompt.png, p1DialogueStemUp.png, p1DialogueStemDown.png, p2DialogueStemUp.png, p2DialogueStemDown.png"; */
float coollerp(float a, float b, float t) {
return lerp(a, b, 1-(1-t)*(1-t));
}
class ConsentPhase
{
//PImage face1Placeholder;
//PImage face2Placeholder;
PImage kissRequestPrompt;
PImage p1KissRequestPrompt;
PImage p1KissRequestText;
PImage p1KissResponseNo;
PImage p1KissResponseYes;
PImage p1DialogueStem;
PImage p2KissRequestPrompt;
PImage p1ResponsePrompt;
PImage p2ResponsePrompt;
PImage p2DialogueStemDown;
int bubbleTimer[] = {0, 0};
int state;
final int CONSENT_NO_BUBBLE = 0;
final int CONSENT_KISS_REQUEST = 1;
final int KISS = 3;
// dialogue
final int REQUEST = 0;
final int RESPONSE_YES = 1;
final int RESPONSE_NO = 2;
int stateTimer;
int initialDelayTimer;
boolean playerAsking[] = {false, false};
int dialogueIndex[] = {0, 0};
//PFont dialogueFont;
ConsentPhase()
{
//face1Placeholder = loadImage("p1FacePlaceholder.png");
//face2Placeholder = loadImage("p2FacePlaceholder.png");
kissRequestPrompt = loadImage("kissRequestPrompt.png");
//p1KissRequestPrompt = loadImage("p1KissRequestPrompt.png");
p1KissRequestText = loadImage("p1KissRequestText.png");
p1KissResponseNo = loadImage("p1KissResponseNo.png");
p1KissResponseYes = loadImage("p1KissResponseYes.png");
p1DialogueStemUp = loadImage("p1DialogueStemUp.png");
p1DialogueStemDown = loadImage("p1DialogueStemDown.png");
//p2KissRequestPrompt = loadImage("p2KissRequestPrompt.png");
p1ResponsePrompt = loadImage("p1ResponsePrompt.png");
p2ResponsePrompt = loadImage("p2ResponsePrompt.png");
p2DialogueStemUp = loadImage("p2DialogueStemUp.png");
p2DialogueStemDown = loadImage("p2DialogueStemDown.png");
// start in the title state
state = CONSENT_NO_BUBBLE;
stateTimer = 0;
initialDelayTimer = 0;
//dialogueFont = loadFont("Tahoma-Bold-48.vlw");
}
void update()
{
stage.step(true);
eyeContact[0] += random(-.2,.2) + .2*sin(1298+millis()*.0017);
eyeContact[1] += random(-.2,.2) + .2*sin(millis()*.001);
eyeContact[0] = constrain(eyeContact[0], 0, 1);
eyeContact[1] = constrain(eyeContact[1], 0, 1);
overrideHeadMotion = false;
headMotionVectorX = 0;
// in this state, players can ask the other for a kiss
if (state == CONSENT_NO_BUBBLE)
{
stateTimer ++;
initialDelayTimer ++;
if (initialDelayTimer > 30)
{
for (int playerIndex = 0; playerIndex < 2; playerIndex ++)
{
if (key1Down[playerIndex] && key2Down[playerIndex])
{
playerAsking[playerIndex] = true;
dialogueIndex[playerIndex] = 0;
bubbleTimer[playerIndex] = min(consentFormTime, bubbleTimer[playerIndex] + 1);
if (bubbleTimer[playerIndex] >= consentFormTime)
{
state = CONSENT_KISS_REQUEST;
stateTimer = 0;
playerAsking[1-playerIndex] = false;
bubbleTimer[1-playerIndex] = 0;
break;
}
}
else
{
playerAsking[playerIndex] = false;
bubbleTimer[playerIndex] = max(0, bubbleTimer[playerIndex] - 1);
}
}
}
}
// in this state, the asker can withdraw the request
// and the other player can respond
if (state == CONSENT_KISS_REQUEST)
{
stateTimer ++;
for (int playerIndex = 0; playerIndex < 2; playerIndex ++)
{
// if asking, they have the option to withdraw their request
if (playerAsking[playerIndex])
{
if ((key1Down[playerIndex] == false) || (key2Down[playerIndex] == false))
{
playerAsking[playerIndex] = false;
bubbleTimer[1-playerIndex] = 0;
state = CONSENT_NO_BUBBLE;
stateTimer = 0;
}
}
// if not asking, they have the option to accept or decline
else
{
// maybe
if (key1Down[playerIndex] && key2Down[playerIndex]) {
bubbleTimer[playerIndex] = max(0, bubbleTimer[playerIndex] - 1);
}
// no
else if (key1Down[playerIndex])
{
bubbleTimer[playerIndex] = min(consentResponseFormTime + holdAfterConsentResponse, bubbleTimer[playerIndex] + 1);
dialogueIndex[playerIndex] = 1;
if (bubbleTimer[playerIndex] >= consentResponseFormTime + holdAfterConsentResponse)
{
// NO, go back to the title screen
gameState = STATE_END;
overrideHeadMotion = false;
//bubbleTimer[0] = 0;
//bubbleTimer[1] = 0;
//state = CONSENT_NO_BUBBLE;
}
else if (bubbleTimer[playerIndex] >= consentResponseFormTime) {
overrideHeadMotion = true;
headMotionVectorX = -1;
}
}
// yes
else if (key2Down[playerIndex])
{
bubbleTimer[playerIndex] = min(consentResponseFormTime + holdAfterConsentResponse, bubbleTimer[playerIndex] + 1);
dialogueIndex[playerIndex] = 2;
if (bubbleTimer[playerIndex] >= consentResponseFormTime + holdAfterConsentResponse)
{
overrideHeadMotion = false;
// YES, start kissing state
gameState = STATE_KISS;
}
else if (bubbleTimer[playerIndex] >= consentResponseFormTime) {
overrideHeadMotion = true;
headMotionVectorX = 1;
}
}
else
{
bubbleTimer[playerIndex] = max(0, bubbleTimer[playerIndex] - 1);
}
}
}
}
}
void render()
{
background(0); noTint();
stage.draw();
if (state == CONSENT_NO_BUBBLE)
{
// if we need to do any per-pixel operations
//loadPixels();
//int targetValue = stateTimer % 43;
//for (int i = 0; i < width * height; i ++)
//{
// pixels[i] = backgroundColor;
//}
//updatePixels();
//text("CONSENT_NO_BUBBLE", 0, height - 64);
//text("p1BubbleTimer = " + bubbleTimer[0], 0, height - 48);
//text("p2BubbleTimer = " + bubbleTimer[1], 0, height - 32);
//drawKeyIndicators();
// asker bubble
for (int playerIndex = 0; playerIndex < 2; playerIndex ++)
{
float blend = min(1.0, bubbleTimer[playerIndex] / consentFormTime);
int left = upperBubbleLeft + headOffset(playerIndex);
int top = upperBubbleTop;
int boxWidth = upperBubbleWidth;
int boxHeight = upperBubbleHeight;
// FIX: when transitioning from CONSENT_KISS_REQUEST to CONSENT_NO_BUBBLE the responding player's bubble pops upward
// possibly store box positions per player and set only on state change?
if (blend > 0.01)
drawDialogueBoxWithContents(left, top, boxWidth, boxHeight, blend, playerIndex, dialogueIndex[playerIndex]);
}
// ADD: special case for simultaneous invitation
// prompts
// individual player kiss request prompts
/*
if (bubbleTimer[0] == 0)
image(p1KissRequestPrompt, 0, height - p1KissRequestPrompt.height);
if (bubbleTimer[1] == 0)
image(p2KissRequestPrompt, width - p2KissRequestPrompt.width, height - p2KissRequestPrompt.height);
*/
// joint prompt
noTint();
image(kissRequestPrompt, 0, height - kissRequestPrompt.height);
}
else if (state == CONSENT_KISS_REQUEST)
{
// asker bubble
for (int playerIndex = 0; playerIndex < 2; playerIndex ++)
{
if (playerAsking[playerIndex])
{
float blend = min(1.0, bubbleTimer[playerIndex] / consentFormTime);
int left = upperBubbleLeft + headOffset(playerIndex);
int top = upperBubbleTop;
int boxWidth = upperBubbleWidth;
int boxHeight = upperBubbleHeight;
if (blend > 0.01);
drawDialogueBoxWithContents(left, top, boxWidth, boxHeight, blend, playerIndex, 0);
}
else
{
// response bubble
float blend = min(1.0, bubbleTimer[playerIndex] / consentResponseFormTime);
if (playerIndex == 0)
left = upperBubbleLeft + headOffset(0);
else
left = upperBubbleLeft + upperBubbleWidth - lowerBubbleWidth + headOffset(1);
top = lowerBubbleTop;
boxWidth = lowerBubbleWidth;
boxHeight = lowerBubbleHeight;
// if nothing is pressed and the box is shrinking, use previous text?
if (blend > 0.01)
drawDialogueBoxWithContents(left, top, boxWidth, boxHeight, blend, playerIndex, dialogueIndex[playerIndex]);
// response prompt
if (bubbleTimer[0] == 0)
{
noTint();
image(p1ResponsePrompt, 0, height - p1ResponsePrompt.height);
}
if (bubbleTimer[1] == 0)
{
noTint();
image(p2ResponsePrompt, width - p2ResponsePrompt.width, height - p2ResponsePrompt.height);
}
}
}
}
//drawTestBoxes();
}
float headOffset(int playerInd) {
float o = -13*(playerInd*2-1);
return (stage.faces.get(playerInd).headBody.GetPosition().x + o)*stage.worldScale.x;
}
// gets correct size, and calls the box and contents draw functions with that size
void drawDialogueBoxWithContents(int left, int top, int boxWidth, int boxHeight, float blend, int playerIndex, int dialogue)
{
// size when newly formed
int initialLeft, initialTop, initialHeight, initialWidth;
int finalLeft, finalTop, finalHeight, finalWidth;
// change start height based on whether this is a high or low bubble
if (top < mouthHeight - 32)
initialTop = mouthHeight - 32;
else
initialTop = mouthHeight + 12;
initialWidth = 60;
initialHeight = 40;
if (playerIndex == 0)
{
initialLeft = centerColumnLeft + headOffset(0);
}
else if (playerIndex == 1)
{
initialLeft = centerColumnLeft + centerColumnWidth - initialWidth + headOffset(1);
}
drawDialogueBox(
coollerp(initialLeft, left, blend),
coollerp(initialTop, top, blend),
coollerp(initialWidth, boxWidth, blend),
coollerp(initialHeight, boxHeight, blend),
playerIndex);
if (dialogue != -1)
drawDialogueBoxContents(
coollerp(initialLeft, left, blend),
coollerp(initialTop, top, blend),
coollerp(initialWidth, boxWidth, blend),
coollerp(initialHeight, boxHeight, blend),
playerIndex,
dialogue);
}
// draws just the contents of the box (all text is images)
void drawDialogueBoxContents(int left, int top, int boxWidth, int boxHeight, int playerIndex, int dialogue)
{
if (playerIndex == 1) // if player 1 is asking
{
tint(p2Color);
}
if (playerIndex == 0) // if player 0 is asking
{
tint(p1Color);
}
PImage sourceImage = p1KissRequestText;
if (dialogue == 1)
sourceImage = p1KissResponseNo;
else if (dialogue == 2)
sourceImage = p1KissResponseYes;
image(sourceImage, left, top, boxWidth, boxHeight);
}
void drawDialogueBox(int left, int top, int width, int height, int playerIndex)
{
noTint();
if (playerIndex == 0)
{
if (top < 280)
image(p1DialogueStemUp, 134 + headOffset(0), 279);
else
image(p1DialogueStemDown, 128 + headOffset(0), 314);
}
else if (playerIndex == 1)
{
if (top < 280)
image(p2DialogueStemUp, 393 + headOffset(1), 279);
else
image(p2DialogueStemDown, 400 + headOffset(1), 314);
}
noStroke();
fill(dialogueBoxColor);
rect(left, top, width, height, 16);
}
}
void drawKeyIndicators()
{
// key indicators (put in final game in some form?)
int centerX = 320;
int spacingX = 32;
int radius = 24;
noTint();
fill(p1Color);
if (key1Down[1])
ellipse(centerX - 1.5 * spacingX, 32, radius, radius);
if (key2Down[1])
ellipse(centerX - 0.5 * spacingX, 32, radius, radius);
fill(p2Color);
if (key1Down[0])
ellipse(centerX + 0.5 * spacingX, 32, radius, radius);
if (key2Down[0])
ellipse(centerX + 1.5 * spacingX, 32, radius, radius);
}
void drawTestBoxes()
{
noFill();
stroke(64);
rect(upperBubbleLeft, upperBubbleTop, upperBubbleWidth, upperBubbleHeight);
rect(upperBubbleLeft, lowerBubbleTop, lowerBubbleWidth, lowerBubbleHeight);
rect(upperBubbleLeft + upperBubbleWidth - lowerBubbleWidth, lowerBubbleTop, lowerBubbleWidth, lowerBubbleHeight);
}
/* @pjs preload="end.jpg"; */
class EndScreen
{
PImage endScreen;
int timer;
EndScreen() {
timer = 0;
endScreen = loadImage("end.jpg");
}
void update() {
timer++;
if (timer > 100) {
gameState = STATE_TITLE;
timer = 0;
}
}
void render() {
noTint();
image(endScreen, 0, 0, width, height);
}
}
/* @pjs preload="titleText.jpg, hand.png, titlePrompts.png"; */
class TitleScreen
{
PImage titleText, titlePrompts;
PImage hand;
int timer;
int handExtend[] = {0, 0};
TitleScreen()
{
timer = 0;
titleText = loadImage("titleText.jpg");
hand = loadImage("hand.png");
titlePrompts = loadImage("titlePrompts.png");
}
void update()
{
int maxExtend = 90;
for (int i = 0; i < 2; i++) {
if (key1Down[i] && key2Down[i]) handExtend[i] += 3;
else handExtend[i]--;
handExtend[i] = max(0, handExtend[i]);
handExtend[i] = min(maxExtend, handExtend[i]);
}
if (handExtend[0] > maxExtend-5 && handExtend[1] > maxExtend-5) {
timer++;
if (timer > 10) {
gameState = STATE_CONSENT;
}
} else {
timer = 0;
}
}
void drawButton(var t, vec3 c, int butW, int x, int y, int letterXoff, int letterYoff, boolean pressed) {
fill(0);
rect(x+2,y-30+2,butW,37);
fill(255,224,162);
int o = 0;
if (pressed) o = 1;
rect(x+o,y-30+o,butW,37);
fill(c.x, c.y, c.z);
text(t, x+2+o+letterXoff, y+o+letterYoff);
}
void render()
{
noTint();
noStroke();
image(titleText, 0, 0);
// draw the first hand
tint(playerColor[0].x, playerColor[0].y, playerColor[0].z);
fill(playerColor[0].x, playerColor[0].y, playerColor[0].z);
rect(0,height-hand.height,handExtend[0]*2,hand.height);
image(hand, handExtend[0]*2, height-hand.height);
// draw the second hand
fill(playerColor[1].x, playerColor[1].y, playerColor[1].z);
rect(width-handExtend[1]*2,height-hand.height,handExtend[1]*2,hand.height);
pushMatrix();
scale(-1,1);
tint(playerColor[1].x, playerColor[1].y, playerColor[1].z);
image(hand, -width+handExtend[1]*2, height-hand.height);
popMatrix();
//fill(playerColor[1].x, playerColor[1].y, playerColor[1].z);
/*int h1 = height - 65;
int h2 = height - 25;
drawButton("A", playerColor[0], 27, 8, h1, 0, 0, key1Down[0]);
drawButton("↑", playerColor[1], 27, width-8-30, h1, 1, -2, key1Down[1]);
drawButton("Z", playerColor[0], 27, 8, h2, 0, 0, key2Down[0]);
drawButton("↓", playerColor[1], 27, width-8-30, h2, 1, -2, key2Down[1]);
*/
noTint();
image(titlePrompts, 0, height-titlePrompts.height,width);
}
}
color backgroundColor = color(0, 0, 0);
color p1Color = color(132, 73, 160);
color p2Color = color(38, 119, 136);
color dialogueBoxColor = color(255, 229, 179);
int consentFormTime = 48;
int consentResponseFormTime = 64;
int holdAfterConsentResponse = 32;
int mouthHeight = 316; // height of center of mouth
int centerColumnLeft = 181;
int centerColumnWidth = 278;
int upperBubbleLeft = 181;
int upperBubbleTop = 58;
int upperBubbleWidth = 278;
int upperBubbleHeight = 252;
int lowerBubbleTop = 326;
int lowerBubbleWidth = 180;
int lowerBubbleHeight = 140;
char p1Key1 = 'q';
char p1Key2 = 'a';
char p2Key1 = 'p';
char p2Key2 = 'l';
// keys 1 and 2, for players 1 and 2; so if key2Down[0] is true, that means the first player (player 0) is pressing their second key.
boolean key1Down[] = { false, false };
boolean key2Down[] = { false, false };
void handleKey(boolean pressed) {
if (key == CODED) {
if (keyCode == UP) {
key1Down[1] = pressed;
} else if (keyCode == DOWN) {
key2Down[1] = pressed;
}
} else {
if (key == 'a' || key == 'A' || key == 's' || key == 'S') {
key1Down[0] = pressed;
} else if (key == 'z' || key == 'Z' || key == 'x' || key == 'X') {
key2Down[0] = pressed;
}
if (key == 'p' || key == 'P') {
key1Down[1] = pressed;
}
if (key == 'l' || key == 'L') {
key2Down[1] = pressed;
}
}
}
void handleMouse(boolean pressed) {
// uncomment if you want mouse as an optional control method
/*if (mouseButton == LEFT) {
key1Down[0] = pressed;
}
if (mouseButton == RIGHT) {
key2Down[0] = pressed;
}*/
}
void keyPressed() {
handleKey(true);
}
void keyReleased() {
handleKey(false);
}
void mousePressed() {
handleMouse(true);
}
void mouseReleased() {
handleMouse(false);
}
// a debug/helper function that returns true if a key (was not pressed last time you called the function AND is now pressed)
boolean pressedLast[] = new boolean[256];
boolean keyHit(char c) {
//if (pressedLast == null) {
// pressedLast = new boolean[256];
//}
//println ("pressedLast[c] == " + pressedLast[c]);
if (keyPressed && key == c) {
//println("down");
if (!pressedLast[c]) {
pressedLast[c] = true;
return true;
} else {
return false;
}
}
pressedLast[c] = false;
return false;
}
// shorthand for common box2d classes
var b2Vec2 = Box2D.Common.Math.b2Vec2
, b2Math = Box2D.Common.Math.b2Math
, b2AABB = Box2D.Collision.b2AABB
, b2BodyDef = Box2D.Dynamics.b2BodyDef
, b2Body = Box2D.Dynamics.b2Body
, b2FixtureDef = Box2D.Dynamics.b2FixtureDef
, b2Fixture = Box2D.Dynamics.b2Fixture
, b2Contact = Box2D.Dynamics.b2Contact
, b2Shape = Box2D.Dynamics.b2Shape
, b2World = Box2D.Dynamics.b2World
, b2MassData = Box2D.Collision.Shapes.b2MassData
, b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape
, b2CircleShape = Box2D.Collision.Shapes.b2CircleShape
, b2DebugDraw = Box2D.Dynamics.b2DebugDraw
, b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef
, b2DistanceJointDef = Box2D.Dynamics.Joints.b2DistanceJointDef
, b2DistanceJoint = Box2D.Dynamics.Joints.b2DistanceJoint
, b2PrismaticJointDef = Box2D.Dynamics.Joints.b2PrismaticJointDef
, b2PrismaticJoint = Box2D.Dynamics.Joints.b2PrismaticJoint
, b2WeldJointDef = Box2D.Dynamics.Joints.b2WeldJointDef
, b2WeldJoint = Box2D.Dynamics.Joints.b2WeldJoint
, b2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef
, b2RevoluteJoint = Box2D.Dynamics.Joints.b2RevoluteJoint;
boolean overrideHeadMotion = false;
float headMotionVectorX = 0, headMotionVectorY = 0;
float eyeContact[] = {0,0};
// --- Parameters for tongue setup/behavior ---
// tweak controls, rendering, etc with these params
int tongueNumSegs = 15;
b2Vec2 tongueCurveRange = new b2Vec2(-.75*15.0/float(tongueNumSegs),.75*15.0/float(tongueNumSegs)); // the min/max angle at each revolute joint in the tongue
float tongueRestCurvature = .03 * (15.0/float(tongueNumSegs));
float tongueShortLengthFactor = .5; // control rest length of tongue (1->shortest, 0->least short)
float tongueLongLengthFactor = 2.5; // control max length of tongue (2->normal max len, longer creates gaps)
float tongueWidthAffectsNeighborWidth = .5; // 0 for no effect, 1 for width entirely follows nbrs
float headMoveForwardRange = 3; // amount head can move forward to follow tongue
float headMoveUpRange = .1; // amount head can move up to follow tongue motion (
float headXMoveSpeedFwd = .1;
float headXMoveSpeedBack = 2;
float headYMoveSpeed = .2;
float tongueSegSpacingX = .9;
float tongueSegSpacingY = .9;
float tongueCurvatureGainFactor = 2; // this just scales the value from a weird custom formula; search for this parameter to see/edit it below
float tongueLengthGain = 2.5;
float tongueWidthGain = 5;
float tongueWidthMotorStrength = 200;
float tongueLengthMotorStrength = 1500;
float tongueCurveMotorStrength = 120000;
float tongueFriction = .01;
float lipGain = 1.5;
float lipMotorTorqueFirstSeg = 5;
float lipMotorTorqueRestSegs = 10000;
int lipNumSegs = 3;
float lipRestCurve = .1;
float lipTotalHeight = .7;
float lipWidth = .35;
float eyeRadius = 2.1;
float eyePupilRadius = .8;
// these determine what collides with what:
int LIP_GROUP_INDEX = -5;
int FACE_GROUP_INDEX_BASE = 1;
int NOSE_GROUP_INDEX = 27;
int EYE_GROUP_INDEX_BASE = 6;
int TONGUE_INDEX_BASE = -1;
int FACE_CATEGORY = 1, LIP_CATEGORY = 2, TONGUE_CATEGORY = 4, EYE_CATEGORY = 8;
int FACE_MASK = 1+4+8, LIP_MASK = 2+4+8, TONGUE_MASK = 1+2+4+8, EYE_MASK=1+2+4+8;
float mouthBottomYOffset = 0;
// global array to track whether tongue is hitting eye
// (sorry for the sloppy coding here; I didn't realize eye needed to know about tongue until too late / too lazy to refactor)
var eyeHit = {};
// the Box2D world that all the simulation uses:
var world;
// the images we build the faces out of:
/* @pjs preload="headbot.png"; */
/* @pjs preload="headtop.png"; */
/* @pjs preload="kissBackground.jpg"; */
PImage headTop, headBot; // top and bottom parts of the head
PImage kissBG;
void drawBody(b2Body b) {
if (useBuffer) {
b2Vec2 p = b.GetPosition();
float a = b.GetAngle();
pg.pushMatrix();
pg.translate(p.x,p.y);
pg.rotate(a);
b2Fixture fix = b.GetFixtureList();
while (fix) {
if (fix.GetType() == 1) {
b2PolygonShape s = fix.GetShape();
pg.beginShape(QUADS);
int n = s.GetVertexCount();
for (int ii = 0; ii < n; ii++) {
b2Vec2 v = s.m_vertices[ii];
pg.vertex(v.x,v.y);
}
pg.endShape();
} else {
b2CircleShape s = fix.GetShape();
b2Vec2 p = s.GetLocalPosition();
float r = s.GetRadius();
pg.ellipse(p.x,p.y,r*2,r*2);
}
fix = fix.GetNext();
}
pg.popMatrix();
} else {
b2Vec2 p = b.GetPosition();
float a = b.GetAngle();
pushMatrix();
translate(p.x,p.y);
rotate(a);
b2Fixture fix = b.GetFixtureList();
while (fix) {
if (fix.GetType() == 1) {
b2PolygonShape s = fix.GetShape();
beginShape(QUADS);
int n = s.GetVertexCount();
for (int ii = 0; ii < n; ii++) {
b2Vec2 v = s.m_vertices[ii];
vertex(v.x,v.y);
}
endShape();
} else {
b2CircleShape s = fix.GetShape();
b2Vec2 p = s.GetLocalPosition();
float r = s.GetRadius();
ellipse(p.x,p.y,r*2,r*2);
}
fix = fix.GetNext();
}
popMatrix();
}
}
void drawBoxWorld(b2World w) {
b2Body b = w.GetBodyList();
while (b) {
drawBody(b);
b = b.GetNext();
}
}
// KissStage sets up the Box2D world and manages both kissing faces
class KissStage {
b2Vec2 worldCenter, worldScale;
ArrayList tongues;
ArrayList faces;
boolean drawDebugToggle = false;
float noInputTimeout;
float fromStartTimer;
boolean bothPlayersPressedKeys;
KissStage() {
headTop = loadImage("headtop.png");
headBot = loadImage("headbot.png");
kissBG = loadImage("kissBackground.jpg");
world = new b2World(new b2Vec2(0, 10), true);
worldCenter = new b2Vec2(width*.5, height*.5);
worldScale = new b2Vec2(width*.5*.1, width*.5*.1);
faces = new ArrayList();
faces.add(new Face(-11.5,2.5,1)); // first face, on the left side of the screen facing right
faces.add(new Face(11.5,2.5,-1)); // second face, on the right side of the screen facing left
noInputTimeout = 0;
fromStartTimer = 0;
p1Active = false;
p2Active = false;
eyeContact[0] = 0; eyeContact[1] = 0;
}
boolean noPauseMode = true;
// do all the input and simulation stuff here
void step(boolean ignoreInput) {
if (!ignoreInput) {
eyeContact[0] = 0; eyeContact[1] = 0;
}
pushMatrix();
//rect(0,0,10,10);
translate(worldCenter.x, worldCenter.y);
scale(worldScale.x, worldScale.y);
if (keyHit('0')) {
noPauseMode = !noPauseMode;
}
// then step sim
if (noPauseMode || keyHit('s')) {
float timeStep = 1.0/30.0;
int stepsPerStep = 4;
timeStep /= stepsPerStep;
for (int i = 0; i < stepsPerStep; i++) {
for (int fi = 0; fi < faces.size(); fi++) {
Face f = (Face)faces.get(fi);
f.step(faces.get(faces.size()-1-fi).tongue, faces.get(faces.size()-1-fi).e, ignoreInput);
}
world.Step(timeStep, 25, 5);
}
}
if (!ignoreInput) {
fromStartTimer++;
}
if (key1Down[0] || key2Down[0])
p1Active = true;
if (key1Down[1] || key2Down[1])
p2Active = true;
// input is accepted but one of the players is not giving input
if (!ignoreInput && fromStartTimer > 100 && p1Active && p2Active &&
( (!key1Down[0] && !key2Down[0]) || (!key1Down[1] && !key2Down[1]) ))
{
noInputTimeout ++;
if (noInputTimeout > 200) {
gameState = STATE_END;
}
} else {
noInputTimeout = 0;
}
popMatrix();
}
void draw() {
image(kissBG, 0, 0, width, height);
if (useBuffer) {
pg.pushMatrix();
pg.translate(worldCenter.x*.5, worldCenter.y*.5);
pg.scale(worldScale.x*.5, worldScale.y*.5);
pg.noStroke();
} else {
pushMatrix();
translate(worldCenter.x, worldCenter.y);
scale(worldScale.x, worldScale.y);
noStroke();
}
if (keyHit('d') || keyHit('D')) {
drawDebugToggle = !drawDebugToggle;
}
if (drawDebugToggle) {
drawBoxWorld(world);
} else {
for (int i = 0; i < faces.size(); i++) {
Face f = (Face)faces.get(i);
f.drawTongue();
}
for (int i = 0; i < faces.size(); i++) {
Face f = (Face)faces.get(i);
f.drawRest(1/worldScale.x);
}
}
if (useBuffer) {
pg.popMatrix();
} else {
popMatrix();
}
}
};
class Face {
Tongue tongue;
Lip lips[];
b2Body headBody;
Eye e;
PVector skinColor;
int xDir;
int faceNum;
float startX, startY;
Face(float x, float y, int xfacing) {
xDir = xfacing;
faceNum = 0;
if (xDir < 0) {
faceNum = 1;
}
skinColor = new PVector(174,0,255);//(128,255,128);
if (faceNum == 1) {
skinColor = new PVector(38,119,136);
}
startX = x; startY = y;
// create head
var bodyDef = new b2BodyDef();
bodyDef.type = b2Body.b2_kinematicBody;
bodyDef.position.Set(x-1.5*xfacing, y);
headBody = world.CreateBody(bodyDef);
// fixtures definition for the head
var fixDef = new b2FixtureDef();
fixDef.filter.groupIndex = FACE_GROUP_INDEX_BASE + faceNum;
fixDef.filter.maskBits = FACE_MASK;
fixDef.filter.categoryBits = FACE_CATEGORY;
fixDef.density = 1.0;
fixDef.friction = 0.1;
fixDef.restitution = 0.01;
fixDef.shape = new b2PolygonShape();
fixDef.shape.SetAsOrientedBox(1, 10, new b2Vec2(-xDir*2,0), 0);
headFixtures = new ArrayList();
headBody.CreateFixture(fixDef); // back of the head (tongue attaches here)
fixDef.shape = new b2CircleShape();
fixDef.shape.SetRadius(1);
fixDef.shape.SetLocalPosition(new b2Vec2(6.45*xfacing,3.0+mouthBottomYOffset+.5));
headBody.CreateFixture(fixDef); // lower jaw circle
fixDef.shape = new b2PolygonShape();
fixDef.shape.SetAsOrientedBox(6.95, 1.25, new b2Vec2(0,3.0+mouthBottomYOffset), 0);
headBody.CreateFixture(fixDef); // lower jaw
fixDef.shape.SetAsOrientedBox(6.5, .5, new b2Vec2(0,3.0+mouthBottomYOffset+1), 0);
headBody.CreateFixture(fixDef); // lower jaw
fixDef.shape.SetAsOrientedBox(6.95, 6, new b2Vec2(0,-6.95), 0);
headBody.CreateFixture(fixDef); // upper jaw / rest of face
fixDef.filter.groupIndex = NOSE_GROUP_INDEX;
fixDef.shape.SetAsOrientedBox(.65, .5, new b2Vec2(7.17*xfacing,-5.4), .65*xfacing);
headBody.CreateFixture(fixDef); // nose
fixDef.shape.SetAsOrientedBox(.3, .5, new b2Vec2(7.7*xfacing,-4.9), 1.1*xfacing);
headBody.CreateFixture(fixDef); // nose 2
fixDef.shape = new b2CircleShape();
fixDef.shape.SetRadius(.5);
fixDef.shape.SetLocalPosition(new b2Vec2(7.85*xfacing,-4.6));
headBody.CreateFixture(fixDef); // nose 3
// add tongue
//start = new b2Vec2(-10,0);
//spacing = new b2Vec2(.5, 1);
tongue = new Tongue(headBody, new b2Vec2(x,y+.2+mouthBottomYOffset*.6), new b2Vec2(xfacing*tongueSegSpacingX, tongueSegSpacingY), skinColor);
// add lips
lips = new Lip[2];
lips[0] = new Lip(headBody, new b2Vec2(x+5.1*xfacing, y-1.3), xfacing, 1);
lips[1] = new Lip(headBody, new b2Vec2(x+5.1*xfacing, y+2.1+mouthBottomYOffset), xfacing, -1);
e = new Eye(headBody, new b2Vec2(x+4*xfacing, y-8.25), faceNum);
}
void step(Tongue otherTongue, Eye otherEye, boolean ignoreInput) {
e.step(otherTongue, otherEye);
tongue.step(ignoreInput);
float tongueExtendFactor =
(tongue.actualLen-tongue.restLen()) / (tongue.longLen()-tongue.restLen());
lips[0].step(tongueExtendFactor);
lips[1].step(tongueExtendFactor);
b2Vec p = headBody.GetWorldPoint(new b2Vec2(xDir*6.2,0));
b2Vec2 target = tongue.tip;
b2Vec2 xRange = new b2Vec2(startX+xDir*5-headMoveForwardRange*faceNum,startX+xDir*5+headMoveForwardRange*(1-faceNum));
b2Vec2 yRange = new b2Vec2(startY-headMoveUpRange,startY);
if (target.x < xRange.x) target.x = xRange.x;
if (target.x > xRange.y) target.x = xRange.y;
if (target.y < yRange.x) target.y = yRange.x;
if (target.y > yRange.y) target.y = yRange.y;
float xgain = headXMoveSpeedFwd, ygain = headYMoveSpeed;
float dx = target.x-p.x, dy = target.y-p.y;
//console.log("log: " + target.x +" "+ startX + " " +dx);
if (abs(dx) < .3) dx = 0;
if (abs(dy) < .3) dy = 0;
if (dx*xDir < 0) xgain = headXMoveSpeedBack;
if (overrideHeadMotion) {
headBody.SetLinearVelocity( new b2Vec2(headMotionVectorX*xDir, headMotionVectorY) );
} else {
headBody.SetLinearVelocity( new b2Vec2( xgain*dx, ygain*dy));
}
}
void drawTongue() {
tongue.draw(skinColor);
}
void drawRest(float scaleFactor) {
b2Vec2 p = headBody.GetPosition();
float a = headBody.GetAngle();
tint(skinColor.x,skinColor.y,skinColor.z);
if (useBuffer) {
// todo: rebuild this code when the below code is finished
} else {
pushMatrix();
translate(p.x-1.6*xDir,p.y-10);
rotate(a);
if (xDir < 0)
scale(-1,1);
image(headTop,0,0,headTop.width*scaleFactor,headTop.height*scaleFactor);
translate(0,373*scaleFactor+mouthBottomYOffset);
image(headBot,0,0,headBot.width*scaleFactor,headBot.height*scaleFactor);
popMatrix();
}
e.draw(skinColor);
//tongue.draw(skinColor);
lips[0].draw(skinColor);
lips[1].draw(skinColor);
noTint();
}
}
class Eye {
b2Body refBody;
b2Body body;
b2Vec2 pupil;
b2WeldJoint joint;
float open;
int faceNum;
Eye(b2Body refBody, b2Vec2 pos, int faceNum) {
float r = eyeRadius;
this.faceNum = faceNum;
pupil = new b2Vec2(0,0);
// create lip body
var bodyDef = new b2BodyDef();
bodyDef.type = b2Body.b2_dynamicBody;
bodyDef.position.Set(pos.x, pos.y);
body = world.CreateBody(bodyDef);
// fixtures definition for the head
var boxDef = new b2FixtureDef();
boxDef.filter.groupIndex = EYE_GROUP_INDEX_BASE+faceNum;
boxDef.filter.maskBits = EYE_MASK;
boxDef.filter.categoryBits = EYE_CATEGORY;
boxDef.density = 0.001;
boxDef.friction = 0.1;
boxDef.restitution = 0.1;
boxDef.shape = new b2CircleShape();
boxDef.shape.SetRadius(r);
body.CreateFixture(boxDef);
b2WeldJointDef jointDef = new b2WeldJointDef();
jointDef.Initialize(refBody, body, pos);
jointDef.collideConnected = false;
joint = world.CreateJoint(jointDef);
}
int flickerTimer = 0;
b2Vec2 tip = new b2Vec2(0,0);
void step(Tongue t, Eye otherEye) {
b2Vec2 p = body.GetPosition();
if (t) {
b2Vec2 target = t.center.get(t.center.size()-1).GetPosition();
target = new b2Vec2(target.x, target.y);
if (otherEye) {
b2Vec2 ep = otherEye.body.GetPosition();
b2Vec2 epp = otherEye.pupil;
target.x = lerp(target.x, ep.x+epp.x, eyeContact[faceNum]);
target.y = lerp(target.y, ep.y+epp.y, eyeContact[faceNum]);
}
tip.x = target.x;
tip.y = target.y;
tip.Subtract(p);
tip.Normalize();
tip.Multiply(eyeRadius - eyePupilRadius*1.1);
//target.Add(p);
pupil.x = tip.x*.05 + pupil.x*.95;
pupil.y = tip.y*.05 + pupil.y*.95;
}
float eyeOpenTarget=(20*abs(sin(steps*.0025)))-1;
if (eyeHit[faceNum]) {
eyeOpenTarget = -20;
flickerTimer = 10;
pupil.x *= .7; pupil.y *= .7;
} else {
flickerTimer--;
}
open = open*.95 + .05*eyeOpenTarget;
steps += 1+random(.1);
}
int steps = random(2791);
void draw(PVector skinColor) {
b2Vec2 bp = body.GetPosition();
// draw the skin of the eyelid
if (useBuffer) {
pg.noStroke();
pg.noTint();
pg.fill(skinColor.x,skinColor.y,skinColor.z);
} else {
noStroke();
noTint();
fill(skinColor.x,skinColor.y,skinColor.z);
}
drawBody(body);
// draw the whites of the eye (clipped according to how open the eye is)
if (useBuffer) {
pg.fill(255,224,164);
} else {
fill(255,224,164);
}
var c = externals.context;
c.save();
c.beginPath();
float o = open;
if (o < 0) {
o = 0;
if (flickerTimer > 0) {
o = random(.01);
}
}
c.rect(bp.x-3,bp.y-eyeRadius*o,6,eyeRadius*2*o);
//c.stroke();
c.clip();
drawBody(body);
// draw the pupil
b2Vec2 pupilPos = new b2Vec2(pupil.x, pupil.y);
pupilPos.Add(bp);
if (useBuffer) {
pg.fill(122,44,2);
pg.ellipse(pupilPos.x,pupilPos.y,eyePupilRadius,eyePupilRadius);
} else {
fill(122,44,2);
ellipse(pupilPos.x,pupilPos.y,eyePupilRadius,eyePupilRadius);
}
c.restore();
}
}
class Lip {
b2Body lipBody; // the first body in the lip
b2RevoluteJoint joint; // the joint connecting the lip to the rest of the mouth
float w, h;
int xDir, yDir;
// in progress: generalize lips to use chain of rigid bodies:
ArrayList bodies, joints; // the rest of the bodies and joints defining the lip
Lip(b2Body connectTo, b2Vec2 jointPos, int xDir, int yDir) {
this.xDir = xDir;
this.yDir = yDir;
w = lipWidth;
fullh = lipTotalHeight;
float bh = fullh / lipNumSegs;
// create lip body
var bodyDef = new b2BodyDef();
bodyDef.type = b2Body.b2_dynamicBody;
bodyDef.position.Set(jointPos.x, jointPos.y);
lipBody = world.CreateBody(bodyDef);
// fixtures definition for the lips
var boxDef = new b2FixtureDef();
boxDef.filter.groupIndex = LIP_GROUP_INDEX;
boxDef.filter.maskBits = LIP_MASK;
boxDef.filter.categoryBits = LIP_CATEGORY;
boxDef.density = 0.005;
boxDef.friction = 0.1;
boxDef.restitution = 0.1;
boxDef.shape = new b2PolygonShape();
boxDef.shape.SetAsOrientedBox(w, bh, new b2Vec2(0,bh*yDir), 0);
lipBody.CreateFixture(boxDef);
var circDef = new b2FixtureDef();
circDef.filter.groupIndex = LIP_GROUP_INDEX;
circDef.filter.maskBits = LIP_MASK;
circDef.filter.categoryBits = LIP_CATEGORY;
circDef.density = 0.005;
circDef.friction = 0.1;
circDef.restitution = 0.1;
circDef.shape = new b2CircleShape();
circDef.shape.SetRadius(w);
circDef.shape.SetLocalPosition(new b2Vec2(0,bh*2*yDir));
lipBody.CreateFixture(circDef);
b2RevoluteJointDef jointDef = new b2RevoluteJointDef();
jointDef.Initialize(connectTo, lipBody, jointPos);
jointDef.enableMotor = true;
jointDef.motorSpeed = 0;
jointDef.maxMotorTorque = lipMotorTorqueFirstSeg;
jointDef.lowerAngle = -PI*.5;
jointDef.upperAngle = PI*.1;
if (xDir*yDir < 0) {
jointDef.lowerAngle = -PI*.1;
jointDef.upperAngle = PI*.5;
}
jointDef.enableLimit = true;
jointDef.collideConnected = false;
joint = world.CreateJoint(jointDef);
bodies = new ArrayList();
joints = new ArrayList();
b2Body lastLipBody = lipBody;
for (int i = 1; i < lipNumSegs; i++) {
jointPos.y += bh*2*yDir;
bodyDef.position.Set(jointPos.x,jointPos.y);
b2Body b = world.CreateBody(bodyDef);
b.CreateFixture(boxDef);
b.CreateFixture(circDef);
bodies.add( b );
b2RevoluteJointDef linkDef = new b2RevoluteJointDef();
linkDef.Initialize(lastLipBody, b, jointPos);
linkDef.enableMotor = true;
linkDef.motorSpeed = 0;
linkDef.maxMotorTorque = lipMotorTorqueRestSegs;
linkDef.lowerAngle = -PI;
linkDef.upperAngle = PI;
linkDef.enableLimit = true;
linkDef.collideConnected = false;
joints.add( world.CreateJoint(linkDef) );
lastLipBody = b;
}
}
void step(float tongueLen) {
float a = joint.GetJointAngle();
float gain = lipGain;
float target = (tongueLen) * -xDir*yDir*PI*.5;
joint.SetMotorSpeed( gain*(target-a) );
for (int i = 0; i < joints.size(); i++) {
b2RevoluteJoint j = joints.get(i);
float aa = j.GetJointAngle();
float tt = -lipRestCurve*xDir*yDir;
float gain2 = gain*5;
j.SetMotorSpeed(gain2*(tt-aa) );
}
}
void draw(PVector skinColor) {
if (useBuffer) {
pg.noStroke();
pg.noTint();
pg.fill(skinColor.x,skinColor.y,skinColor.z);
} else {
noStroke();
noTint();
fill(skinColor.x,skinColor.y,skinColor.z);
}
drawBody(lipBody);
for (int i = 0; i < bodies.size(); i++) {
drawBody(bodies.get(i));
}
ellipse(joint.GetAnchorA().x, joint.GetAnchorA().y, w*2, w*2);
}
}
class Tongue {
// all the rigid bodies
ArrayList center;
ArrayList thickness[]; // bodies on the sides of the tongue, that push out from the center for thickness
// all the joints
ArrayList hinges; // rotational joints
ArrayList extenders; // prismatic joints to extend the tongue
ArrayList thickeners[][]; // prismatic joints to thicken the tongue
// the body that the back of the tongue is attached to
b2Body base; //the body that the base of the tongue gets attached to
PVector c;
float thinThickeners[][];
// parameters defining the location, length, etc of the tongue
b2Vec2 start, spacing;
b2Vec2 boxSize;
int numSegs;
// shape parameters, controlled by user input, that define how the tongue shape changes
float curve; // target angle at each hinge joint
b2Vec2 curveRange = tongueCurveRange;
float len; // target length of the tongue
float actualLen;
// define which keys control this tongue
int keyCodeUp, keyCodeDown;
int flippedTongue;
int tonguePlayer;
float bodyMass = 1;
b2Vec2 tip;
Tongue(b2Body base, b2Vec2 st, b2Vec2 sp, PVector skinColor) {
this.base = base;
numSegs = tongueNumSegs;
start = st;
spacing = sp;
boxSize = new b2Vec2(abs(sp.x*.5), sp.y*.5);
flippedTongue = 1;
tonguePlayer = 1;
keyCodeUp = 'a';
keyCodeDown = 'z';
if (sp.x < 0) {
flippedTongue = -1;
tonguePlayer = 0;
}
c = new PVector(208,82,82);
float skinColorWt = .2;
c.add(PVector.mult(skinColor,skinColorWt));
c.mult(1.0/(1+skinColorWt));
//if (tonguePlayer == 1) {
// c = new PVector(208,82,191);
//}
build(numSegs, start, spacing);
}
void build(int segs, b2Vec2 st, b2Vec2 sp) {
numSegs = segs;
start = st;
spacing = sp;
center = new ArrayList();
thickness = new ArrayList[2]; // bodies on the sides of the tongue, that push out from the center for thickness
thickness[0] = new ArrayList();
thickness[1] = new ArrayList();
hinges = new ArrayList(); // rotational joints
extenders = new ArrayList(); // prismatic joints to extend the tongue
thickeners = new ArrayList[2]; // prismatic joints to thicken the tongue
thickeners[0] = new ArrayList();
thickeners[1] = new ArrayList();
thinThickeners = new float[numSegs][2];
for (int side = 0; side < 2; side++) {
for (int i = 0; i < numSegs; i++) {
thinThickeners[side][i] = 0;
}
}
b2FixtureDef fix = new b2FixtureDef();
fix.density = .1;
fix.friction = tongueFriction;
fix.restitution = 0.2;
b2Shape midRect = new b2PolygonShape();
fix.shape = midRect; fix.shape.SetAsBox(boxSize.x*(tongueLongLengthFactor-1), boxSize.y);
b2Shape thinRect0 = new b2PolygonShape(), thinRect1 = new b2PolygonShape();
float slopFactor = 4;
thinRect1.SetAsOrientedBox(boxSize.x*(tongueLongLengthFactor-1)*1.2, boxSize.y-slopFactor*Box2D.Common.b2Settings.b2_linearSlop,
new b2Vec2(0,slopFactor*Box2D.Common.b2Settings.b2_linearSlop), 0);
thinRect0.SetAsOrientedBox(boxSize.x*(tongueLongLengthFactor-1)*1.2, boxSize.y-slopFactor*Box2D.Common.b2Settings.b2_linearSlop,
new b2Vec2(0,-slopFactor*Box2D.Common.b2Settings.b2_linearSlop), 0);
b2Shape thinRectOff = new b2PolygonShape();
thinRectOff.SetAsOrientedBox(boxSize.x*(tongueLongLengthFactor-1)*1.2, boxSize.y-slopFactor*Box2D.Common.b2Settings.b2_linearSlop,
new b2Vec2(-flippedTongue*boxSize.x*(tongueLongLengthFactor-1)*.4,0), 0);
//fixDef.shape = new b2CircleShape(); fixDef.shape.SetRadius(boxSize.x);
fix.filter.groupIndex = TONGUE_INDEX_BASE - tonguePlayer;
fix.filter.maskBits = TONGUE_MASK;
fix.filter.categoryBits = TONGUE_CATEGORY;
//fix.angularDamping = 1;
//fix.linearDamping = 1;
b2CircleShape endCircle1 = new b2CircleShape();
b2CircleShape endCircle2 = new b2CircleShape();
endCircle1.SetRadius(boxSize.y);
endCircle2.SetRadius(boxSize.y);
endCircle1.SetLocalPosition(new b2Vec2(-boxSize.x*1.5*flippedTongue));
endCircle2.SetLocalPosition(new b2Vec2(boxSize.x*1.5*flippedTongue));
bodyDef = new b2BodyDef();
bodyDef.type = b2Body.b2_dynamicBody;
boolean addEndCaps = false;
float x = start.x;
for (int i = 0; i < numSegs; i++) {
bodyDef.position.Set(x, start.y);
b2Body b = world.CreateBody(bodyDef);
fix.density = .01;
b.CreateFixture(fix);
bodyMass = b.GetMass();
center.add(b);
for (int side = 0; side < 2; side++) {
bodyDef.position.Set(x,start.y-2.5*boxSize.y*(side*2-1));
b2Body thb = world.CreateBody(bodyDef);
fix.density = .001;
fix.shape = midRect;
thb.CreateFixture(fix);
if (i < numSegs) {
fix.isSensor = true;
if (i+1 == numSegs) {
fix.shape = thinRectOff;
} else if (side == 0) {
fix.shape = thinRect0;
} else {
fix.shape = thinRect1;
}
fix.userData = (side*2-1)*(i+1) + 10000*tonguePlayer;
thb.CreateFixture(fix);
fix.userData = null;
fix.isSensor = false;
}
if (addEndCaps && i > 0 && i + 1 < numSegs) {
fix.shape = endCircle1;
thb.CreateFixture(fix);
fix.shape = endCircle2;
thb.CreateFixture(fix);
}
thickness[side].add(thb);
fix.shape = midRect;
}
if (i % 2 == 1) {
x += spacing.x;
} else {
x -= spacing.x*tongueShortLengthFactor;
}
}
b2Body connectTo = base;
for (int i = 0; i < numSegs; i++) {
b2Body b = (b2Body)center.get(i);
if (i%2 == 0) { // rotation joints (for curving the tongue)
b2RevoluteJointDef jointDef = new b2RevoluteJointDef();
b2Vec2 bp = b.GetPosition();
jointDef.Initialize(connectTo, b, new b2Vec2(bp.x-spacing.x,bp.y));
jointDef.enableMotor = true;
jointDef.motorSpeed = 0;
jointDef.maxMotorTorque = tongueCurveMotorStrength;
jointDef.lowerAngle = curveRange.x;
jointDef.upperAngle = curveRange.y;
jointDef.enableLimit = true;
jointDef.collideConnected = false;
hinges.add(world.CreateJoint(jointDef));
} else { // extender joints (for lengthening the tongue)
b2PrismaticJointDef prismDef = new b2PrismaticJointDef();
b2Vec2 bp = b.GetPosition();
prismDef.Initialize(connectTo, b, bp, new b2Vec2(1,0));
prismDef.lowerTranslation = 0;
prismDef.upperTranslation = spacing.x*2;
if (prismDef.upperTranslation < prismDef.lowerTranslation) {
prismDef.lowerTranslation = spacing.x*2;
prismDef.upperTranslation = 0;
}
prismDef.enableLimit = true;
prismDef.maxMotorForce = tongueLengthMotorStrength*bodyMass;
prismDef.motorSpeed = 0;
prismDef.enableMotor = true;
extenders.add(world.CreateJoint(prismDef));
}
for (int side = 0; side < 2; side++) { // thickening joints
b2PrismaticJointDef prismDef = new b2PrismaticJointDef();
b2Body th = (b2Body)thickness[side].get(i);
b2Vec2 bp = b.GetPosition();
prismDef.Initialize(th, b, bp, new b2Vec2(0,1));
prismDef.lowerTranslation = 0;
prismDef.upperTranslation = boxSize.y*2.5;
prismDef.enableLimit = true;
prismDef.maxMotorForce = tongueWidthMotorStrength*bodyMass;
prismDef.motorSpeed = 0;
if (side == 1) {
prismDef.lowerTranslation = -boxSize.y*2.5;
prismDef.upperTranslation = 0;
}
prismDef.enableMotor = true;
thickeners[side].add(world.CreateJoint(prismDef));
}
connectTo = b;
}
}
float floppiness = 0;
int stepCount = 0;
void step(boolean ignoreInput) {
stepCount++;
// gradually re-thicken tongue segments
for (int side = 0; side < 2; side++) {
for (int i = 0; i < numSegs; i++) {
thinThickeners[side][i] *= .99;
}
}
// default eyeHit array to false; when we detect eye hits in the below loop we will set it back to true as needed
eyeHit[0] = false; eyeHit[1] = false;
// detect which segments of the tongue we should thin
for (b2Contact c = world.GetContactList(); c; c = c.GetNext()) {
if (c.IsTouching()) {
b2Fixture a = c.GetFixtureA();
b2Fixture b = c.GetFixtureB();
int agi = a.GetFilterData().groupIndex;
int bgi = b.GetFilterData().groupIndex;
boolean tongueInvolved = (agi == -1 || agi == -2
|| bgi == -1 || bgi == -2);
if (tongueInvolved) {
//if (agi > 4 || bgi > 4) {
//console.log("tongue eye " + agi + " " + bgi);
//}
int eh = -1;
int uEye = agi - EYE_GROUP_INDEX_BASE;
if (uEye < 2 && uEye >= 0) {
eh = uEye;
} else {
uEye = bgi - EYE_GROUP_INDEX_BASE;
if (uEye < 2 && uEye >= 0) {
eh = uEye;
}
}
if (eh > -1) {
eyeHit[eh] = true;
}
}
/*if (a.GetFilterData().groupIndex > 0 || b.GetFilterData().groupIndex > 0) {
continue;
}*/
if (a.IsSensor() && b.IsSensor()) {
continue;
}
if (a.IsSensor() || b.IsSensor()) { // it's a tongue thinner!
int u = a.GetUserData();
b2Fixture tongueFix = a, otherFix = b;
if (b.IsSensor()) {
u = b.GetUserData();
tongueFix = b; otherFix = a;
}
int player = 0;
if (u > 1000) {
player = 1;
u -= 10000;
}
if (player != tonguePlayer) {
continue;
}
if ((1-tonguePlayer) == otherFix.GetFilterData().groupIndex-FACE_GROUP_INDEX_BASE) { // ignore tongue bits colliding with own face
continue;
}
/* // debug vis. of where the collisions are that will thin the tongue
b2Vec2 ap = a.GetBody().GetPosition();
b2Vec2 bp = b.GetBody().GetPosition();
stroke(255,0,255);
strokeWeight(.1);
fill(0,255,0);
//line(ap.x,ap.y,bp.x,bp.y);
ellipse(ap.x,ap.y,.6,.6);
fill(0,255,255);
ellipse(bp.x,bp.y,.6,.6);
*/
int side = 1;
if (u < 0) {
u *= -1;
side = 0;
}
u -= 1;
thinThickeners[side][u] = thinThickeners[side][u]*.9 + .1;
}
}
}
boolean input = false;
if (!ignoreInput) {
if (key1Down[1-tonguePlayer]) {
input = true;
curve -= .025;
}
if (key2Down[1-tonguePlayer]) {
input = true;
curve += .025;
}
if (key1Down[1-tonguePlayer] && key2Down[1-tonguePlayer]) {
if (curve > .025) curve -= .025;
else if (curve < -.025) curve += .025;
else curve *= .9;
}
}
if (input) {
len = len*.99 + .01*2;
floppiness = floppiness*.95;
} else {
len = len*.99;
curve += .01;
float targetRestCurve = tongueRestCurvature;
if (curve > targetRestCurve) { curve -= .02; }
if (abs(curve - targetRestCurve) < .01) curve = targetRestCurve;
floppiness = floppiness*.9 + .1;
}
if (curve < curveRange.x) {
curve = curveRange.x;
}
if (curve > curveRange.y) {
curve = curveRange.y;
}
// update box2d motors
// tongue curvature
for (int i = 0; i < hinges.size(); i++) {
b2RevoluteJoint j = (b2RevoluteJoint)hinges.get(i);
float a = j.GetJointAngle();
float target = curve*flippedTongue;
if (i == 0) target = 0;
float gain = (1) * ((1-floppiness)*.5 + .5);
if (i < 2) gain = 2;
else if (i > 7) gain *= 100;
else if (i > 6) gain *= 10;
else if (i > 5) gain *= 3;
else gain *= 1.2;
float spd = (target-a)*gain*tongueCurvatureGainFactor;
j.SetMotorSpeed(spd);
if (i > 0) {
float torqueBoost = (hinges.size()-i)*.5+1;
j.SetMaxMotorTorque(((1-floppiness)*.8+.8) * bodyMass * 600 * torqueBoost);
}
}
// tongue length
for (int i = 0; i < extenders.size(); i++) {
b2PrismaticJoint j = (b2PrismaticJoint)extenders.get(i);
float jlen = j.GetJointTranslation();
float target = len*flippedTongue;
float gain = tongueLengthGain;
j.SetMotorSpeed(gain * (target-jlen));
}
// tongue thickness
float distAlong = 0;
b2Vec2 lastbp;
for (int i = 0; i < center.size(); i++) {
b2Body b = (b2Body)center.get(i);
b2Vec2 bp = b.GetPosition();
if (i > 0) {
b2Vec2 bd = new b2Vec2(lastbp.x-bp.x, lastbp.y-bp.y);
distAlong += sqrt(bd.x*bd.x+bd.y*bd.y);
}
float targetWidth = heightAlongTongue(distAlong)*(tongueLongLengthFactor/2);
for (int side = 0; side < 2; side++) {
b2PrismaticJoint j = (b2PrismaticJoint)thickeners[side].get(i);
float jt = j.GetJointTranslation();
float nbrAvg = targetWidth;
if (i > 2) {
nbrAvg = thickeners[side].get(i-1).GetJointTranslation();
}
float target = targetWidth*(1-tongueWidthAffectsNeighborWidth)+nbrAvg*tongueWidthAffectsNeighborWidth;
float thinTarget = -1*(side*2-1);
float tt = thinThickeners[side][i];
target = (1-tt)*target + tt*thinTarget;
float gain = tongueWidthGain;
j.SetMotorSpeed(gain * (target-jt));
targetWidth = -targetWidth;
}
lastbp = bp;
}
actualLen = distAlong;
tip = center.get(numSegs-1).GetWorldPoint(new b2Vec2(spacing.x*.5, 0));
}
float restLen() {
return boxSize.x * numSegs;
}
float longLen() {
return boxSize.x * numSegs*tongueLongLengthFactor;
}
float heightAlongTongue(float distance) {
float minDist = restLen();
float maxDist = longLen();
float curLen = (1+len)*minDist;
float minH = .8-( (curLen-minDist)/(maxDist-minDist) )*.8;
if (distance > curLen) return 1-minH;
return ( distance/curLen )*(distance/curLen)*(1-minH); // linear shrinkage of tongue width along tongue length
}
void drawFlesh() {
float edgeSize = 1;
float edgeSizeOffset = .5*edgeSize;
float offsetX = edgeSize*.5;
if (useBuffer) {
pg.stroke(c.x,c.y,c.z);
pg.strokeWeight(edgeSize);
pg.strokeJoin(ROUND);
pg.strokeCap(ROUND);
pg.fill(c.x,c.y,c.z); // tongue color
} else {
stroke(c.x,c.y,c.z);
strokeWeight(edgeSize);
//strokeWeight(.05);
strokeJoin(ROUND);
strokeCap(ROUND);
noFill();
fill(c.x,c.y,c.z); // tongue color
}
// draw method 1:
// no smoothing for now; just doing the obvious thing
//texture(tongueImage); // note: texture only works in P3D mode; not sure if we want that
/*
beginShape(TRIANGLE_STRIP);
for (int i = 0; i < center.size(); i+=2) {
for (int side = 0; side < 2; side++) {
b2Vec2 local = new b2Vec2(spacing.x*.5,boxSize.y * (1-side*2) * -1);
b2Body b = (b2Body)thickness[side].get(i);
b2Vec2 bp = b.GetWorldPoint(local);
vertex(bp.x,bp.y);//, tongueImage.width*i/(center.size()-1), tongueImage.height*side );
}
}
endShape();
*/
// draw method 2:
// build arrays of control points and send them to smoothTube function
ArrayList tongueSides[] = new ArrayList[2];
tongueSides[0] = new ArrayList();
tongueSides[1] = new ArrayList();
for (int side = 0; side < 2; side++) {
b2Vec2 local = new b2Vec2(-spacing.x*2,(boxSize.y-edgeSizeOffset) * (1-side*2) );
b2Body b = (b2Body)thickness[side].get(0);
b2Vec2 bp = b.GetWorldPoint(local);
tongueSides[side].add(bp);
}
for (int i = 0; i < center.size(); i+=2) {
for (int side = 0; side < 2; side++) {
float offsetX = edgeSize*.5;
b2Vec2 local = new b2Vec2(spacing.x*.75-offsetX*flippedTongue,(boxSize.y-edgeSizeOffset) * (1-side*2) );
b2Body b = (b2Body)thickness[side].get(i);
b2Vec2 bp = b.GetWorldPoint(local);
tongueSides[side].add(bp);
}
}
drawSmoothedTube(tongueSides[0], tongueSides[1], 5, 0);
// draw method 3:
// follow tongue curves & send vertices to drawShape
/*
ArrayList pts = new ArrayList();
b2Vec2 incomingTangent = new b2Vec2(1,0);
int side = 0;
b2Vec2 local = new b2Vec2(spacing.x*.75-offsetX*flippedTongue,(boxSize.y-edgeSizeOffset) * (1-side*2) );
b2Vec2 localMid = new b2Vec2(spacing.x*.25-offsetX*flippedTongue,(boxSize.y-edgeSizeOffset) * (1-side*2) );
b2Vec2 incomingPt = thickness[side].get(0).GetWorldPoint(local);
pts.add(new b2Vec2(incomingPt.x-flippedTongue*10, incomingPt.y));
b2Vec2 lastPt = incomingPt;
b2Vec2 lastTang = incomingTangent;
for (side = 0; side < 2; side++) {
local = new b2Vec2(spacing.x*.75-offsetX*flippedTongue,(boxSize.y-edgeSizeOffset) * (1-side*2) );
localMid = new b2Vec2(spacing.x*0-offsetX*flippedTongue,(boxSize.y-edgeSizeOffset) * (1-side*2) );
int idir = -1*(side*2-1);
int istart = (side)*(center.size()-2);
for (int i = istart; ((i+1) < center.size()) && ((i) >= 0); i += idir) {
b2Vec2 p;
if (i % 2 == 0) {
p = thickness[side].get(i).GetWorldPoint(local);
} else {
p = thickness[side].get(i).GetWorldPoint(localMid);
if (abs(extenders.get(int(i/2)).GetJointTranslation()) 0) {
scalem1 = .5;
a0 = pts.get(i-1);
}
float scalem2 = 1;
if (i < n-2) {
scalem2 = .5;
d1 = pts.get(i+2);
}
b2Vec2 m1 = b2Math.SubtractVV(d,a0);
b2Vec2 m2 = b2Math.SubtractVV(d1,a);
m1.Multiply(bezTangentScale*scalem1/3.0);
m2.Multiply(bezTangentScale*scalem2/3.0);
b2Vec2 b = b2Math.AddVV(m1,a);
b2Vec2 c = b2Math.SubtractVV(d,m2);
/* // debugging vis of control points
stroke(50);
ellipse(b.x,b.y,.1,.1);
stroke(100,0,100);
ellipse(c.x,c.y,.1,.1);
noStroke();*/
for (int ii = 1; ii < samplesPerSeg; ii++) {
float t = ((float)ii) / ((float)samplesPerSeg);
b2Vec2 p = interpBez(a,b,c,d,t);
toRet.add(p);
}
//toRet.add(b);
//toRet.add(c);
toRet.add(d);
}
return toRet;
}
// draws a smooth tube by putting a spline through the top and bottom point arrays
void drawSmoothedTube(ArrayList top, ArrayList bot, int samplesPerSeg, int extraPointHack) {
// if extraPointHack is not zero, adds extra points that are just useful for the tongue to extend back off-screen
// (I no longer use / need extraPointHack; it is vestigial)
ArrayList tsmooth = smoothPointList(top, samplesPerSeg, 1);
ArrayList bsmooth = smoothPointList(bot, samplesPerSeg, 1);
int n = tsmooth.size();
/*beginShape(TRIANGLE_STRIP);
//beginShape(LINES);
for (int i = 0; i < n; i++) {
b2Vec2 p = tsmooth.get(i);
//ellipse(p.x,p.y,.2,.2);
vertex(p.x,p.y);
p = bsmooth.get(i);
//ellipse(p.x,p.y,.2,.2);
vertex(p.x,p.y);
}
endShape();
//noFill();
//stroke(255);
*/
if (useBuffer) {
pg.beginShape();
b2Vec2 p;
if (extraPointHack != 0) {
p = tsmooth.get(0);
pg.vertex(p.x+extraPointHack*10,p.y);
}
for (int i = 0; i < n; i++) {
p = tsmooth.get(i);
pg.vertex(p.x,p.y);
}
for (int i = n-1; i >= 0; i--) {
p = bsmooth.get(i);
pg.vertex(p.x,p.y);
}
if (extraPointHack != 0) {
p = bsmooth.get(0);
pg.vertex(p.x+extraPointHack*10,p.y);
}
pg.endShape(CLOSE);
} else {
beginShape();
b2Vec2 p;
if (extraPointHack != 0) {
p = tsmooth.get(0);
vertex(p.x+extraPointHack*10,p.y);
}
for (int i = 0; i < n; i++) {
p = tsmooth.get(i);
vertex(p.x,p.y);
}
for (int i = n-1; i >= 0; i--) {
p = bsmooth.get(i);
vertex(p.x,p.y);
}
if (extraPointHack != 0) {
p = bsmooth.get(0);
vertex(p.x+extraPointHack*10,p.y);
}
endShape(CLOSE);
}
}
/*
planning file:
I don't know:
- javascript anti-aliases everything it draws, I cannot make it stop :(
so the only way I can see to get a pixel look is by drawing to a low res buffer and upscaling
but then it's still anti-aliased, just at a lower res
OR we could just embrace the fancy high resolution anti-aliased 640x480 future
to add:
- back of mouth
- properly import face images
- color tinting of faces + tongues based on player selections
- intro/player-select screen, consent screen
ideas:
- tongues can fall out if kiss lasts more than a certain amount of time? (they then explore world below)
blocked:
- physics objects defining face don't match image of face (don't fix until face is more final)
skipping for now:
- to make the tongue smooth, I skip some details;
this leads to imperfect-looking tongue physics, because the collisions in physics don't match the collisions the user sees
in theory I could fix this in a couple of different ways (coming at it via rendering or physics)
for now it doesn't seem like a huge deal to me.
*/