{
  "case_id": "B1",
  "case_name": "情绪台球游戏",
  "prompt": "创建一个3D台球游戏，特殊规则：每颗球都带有\"情绪值\"，要求：\n1. 使用Three.js实现物理引擎\n2. 情绪值系统：\n   - 愤怒球（红色）：移动速度快，容易失控乱撞\n   - 忧郁球（蓝色）：移动缓慢，容易停滞\n   - 兴奋球（黄色）：反弹力度大，难以预测\n3. 球的情绪会相互传染（碰撞后改变）\n4. 显示每颗球的情绪条\n5. 单文件HTML",
  "result": {
    "success": true,
    "content": "这是一个包含完整逻辑的单文件HTML代码。通过结合 **Three.js** (渲染) 和 **Cannon-es** (物理引擎) 实现了你要求的3D台球游戏及情绪系统。\n\n### 游戏特色说明：\n1.  **物理引擎**：使用 Cannon-es 模拟真实的碰撞、滚动和反弹。\n2.  **情绪系统**：\n    *   **愤怒 (红)**：只要在移动，就会随机产生加速冲量，且阻力极小，非常暴躁。\n    *   **忧郁 (蓝)**：阻力极大，移动像是在泥沼中，很快就会停下。\n    *   **兴奋 (黄)**：碰撞时会获得额外的反弹力，导致轨迹难以预测。\n3.  **传染机制**：当两球碰撞时，动能较大（更强势）的一方会将自己的情绪“传染”给另一方。\n4.  **UI显示**：每颗球上方都有跟随的HTML血条/情绪条，显示当前情绪颜色。\n5.  **操作**：按住鼠标左键拖拽来蓄力，松开打击母球（白球）。\n\n### 代码文件 (index.html)\n\n你可以直接将以下代码保存为 `.html` 文件并在浏览器中打开（建议使用 Chrome 或 Edge）。\n\n```html\n<!DOCTYPE html>\n<html lang=\"zh\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>情绪台球 3D</title>\n    <style>\n        body { margin: 0; overflow: hidden; background-color: #1a1a1a; font-family: sans-serif; }\n        #game-container { position: relative; width: 100vw; height: 100vh; }\n        canvas { display: block; }\n        \n        /* UI 层 */\n        #ui-layer {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none; /* 让鼠标穿透UI层操作3D场景 */\n        }\n\n        .ball-label {\n            position: absolute;\n            transform: translate(-50%, -100%);\n            width: 60px;\n            text-align: center;\n            transition: top 0.1s, left 0.1s;\n        }\n\n        .emotion-bar-bg {\n            width: 100%;\n            height: 6px;\n            background: rgba(0,0,0,0.5);\n            border-radius: 3px;\n            overflow: hidden;\n            border: 1px solid rgba(255,255,255,0.3);\n        }\n\n        .emotion-bar-fill {\n            height: 100%;\n            width: 100%;\n            transition: background-color 0.3s;\n        }\n\n        .emotion-text {\n            font-size: 10px;\n            color: white;\n            text-shadow: 1px 1px 2px black;\n            margin-bottom: 2px;\n        }\n\n        #instructions {\n            position: absolute;\n            top: 20px;\n            left: 20px;\n            color: white;\n            background: rgba(0,0,0,0.7);\n            padding: 15px;\n            border-radius: 8px;\n            pointer-events: none;\n            user-select: none;\n        }\n        h2 { margin-top: 0; }\n        .legend span { display: inline-block; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%; }\n    </style>\n</head>\n<body>\n\n<div id=\"game-container\">\n    <div id=\"instructions\">\n        <h2>情绪台球</h2>\n        <p>拖拽鼠标蓄力，松开击打白球。</p>\n        <div class=\"legend\"><span style=\"background:#ff3333\"></span>愤怒: 暴躁加速，难以控制</div>\n        <div class=\"legend\"><span style=\"background:#3333ff\"></span>忧郁: 移动缓慢，极易停滞</div>\n        <div class=\"legend\"><span style=\"background:#ffff33\"></span>兴奋: 强力反弹，轨迹诡异</div>\n        <p>规则: 碰撞时，动能大的球会传染情绪。</p>\n    </div>\n    <div id=\"ui-layer\"></div>\n</div>\n\n<!-- 引入 Three.js 和 Cannon-es -->\n<script type=\"importmap\">\n    {\n        \"imports\": {\n            \"three\": \"https://unpkg.com/three@0.160.0/build/three.module.js\",\n            \"three/addons/\": \"https://unpkg.com/three@0.160.0/examples/jsm/\",\n            \"cannon-es\": \"https://unpkg.com/cannon-es@0.20.0/dist/cannon-es.js\"\n        }\n    }\n</script>\n\n<script type=\"module\">\n    import * as THREE from 'three';\n    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';\n    import * as CANNON from 'cannon-es';\n\n    // --- 配置 ---\n    const BALL_RADIUS = 1;\n    const TABLE_WIDTH = 30;\n    const TABLE_HEIGHT = 50;\n    \n    // 情绪类型定义\n    const EMOTIONS = {\n        NORMAL: { color: 0xffffff, name: '平静', damping: 0.3, restitution: 0.7 },\n        ANGRY: { color: 0xff3333, name: '愤怒', damping: 0.1, restitution: 0.8 }, // 阻力小，反弹一般\n        SAD: { color: 0x3333ff, name: '忧郁', damping: 0.8, restitution: 0.1 },   // 阻力极大，不反弹\n        EXCITED: { color: 0xffff33, name: '兴奋', damping: 0.3, restitution: 1.5 } // 反弹系数超高\n    };\n\n    // --- 全局变量 ---\n    let scene, camera, renderer, world, controls;\n    let balls = [];\n    let raycaster = new THREE.Raycaster();\n    let mouse = new THREE.Vector2();\n    let isDragging = false;\n    let dragStart = new THREE.Vector2();\n    let cueBall;\n    let lineGuide;\n\n    // --- 初始化 ---\n    function init() {\n        // 1. Three.js 场景\n        scene = new THREE.Scene();\n        scene.background = new THREE.Color(0x222222);\n\n        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);\n        camera.position.set(0, 40, 40);\n        camera.lookAt(0, 0, 0);\n\n        renderer = new THREE.WebGLRenderer({ antialias: true });\n        renderer.setSize(window.innerWidth, window.innerHeight);\n        renderer.shadowMap.enabled = true;\n        document.getElementById('game-container').appendChild(renderer.domElement);\n\n        // 灯光\n        const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);\n        scene.add(ambientLight);\n        const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);\n        dirLight.position.set(10, 50, 20);\n        dirLight.castShadow = true;\n        dirLight.shadow.camera.top = 30;\n        dirLight.shadow.camera.bottom = -30;\n        dirLight.shadow.camera.left = -30;\n        dirLight.shadow.camera.right = 30;\n        scene.add(dirLight);\n\n        // 2. Cannon.js 物理世界\n        world = new CANNON.World();\n        world.gravity.set(0, -98.2, 0); // 重力\n        world.broadphase = new CANNON.NaiveBroadphase();\n        world.solver.iterations = 10;\n\n        // 物理材质\n        const defaultMaterial = new CANNON.Material('default');\n        const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial, defaultMaterial, {\n            friction: 0.3,\n            restitution: 0.7\n        });\n        world.addContactMaterial(defaultContactMaterial);\n\n        // 3. 创建桌子\n        createTable();\n\n        // 4. 创建球\n        createBalls();\n\n        // 5. 辅助线\n        const lineGeo = new THREE.BufferGeometry();\n        const lineMat = new THREE.LineBasicMaterial({ color: 0xffffff });\n        lineGuide = new THREE.Line(lineGeo, lineMat);\n        scene.add(lineGuide);\n\n        // 6. 控制器\n        controls = new OrbitControls(camera, renderer.domElement);\n        controls.enableDamping = true;\n        controls.dampingFactor = 0.05;\n        controls.maxPolarAngle = Math.PI / 2 - 0.1; // 防止相机穿过桌子\n\n        // 7. 事件监听\n        window.addEventListener('resize', onWindowResize);\n        window.addEventListener('mousedown', onMouseDown);\n        window.addEventListener('mousemove', onMouseMove);\n        window.addEventListener('mouseup', onMouseUp);\n\n        animate();\n    }\n\n    function createTable() {\n        // 视觉部分\n        const tableGeo = new THREE.BoxGeometry(TABLE_WIDTH, 2, TABLE_HEIGHT);\n        const tableMat = new THREE.MeshStandardMaterial({ color: 0x006600 }); // 绿色台呢\n        const tableMesh = new THREE.Mesh(tableGeo, tableMat);\n        tableMesh.position.y = -1;\n        tableMesh.receiveShadow = true;\n        scene.add(tableMesh);\n\n        // 物理地面\n        const groundShape = new THREE.BoxGeometry(TABLE_WIDTH, 2, TABLE_HEIGHT); // 使用Box作为地面防止穿透\n        const groundBody = new CANNON.Body({\n            mass: 0, // 静态\n            shape: new CANNON.Box(new CANNON.Vec3(TABLE_WIDTH/2, 1, TABLE_HEIGHT/2)),\n            position: new CANNON.Vec3(0, -1, 0)\n        });\n        world.addBody(groundBody);\n\n        // 墙壁 (Cushions)\n        const wallThickness = 2;\n        const wallHeight = 2;\n        \n        const createWall = (x, z, w, d) => {\n            // 视觉\n            const wGeo = new THREE.BoxGeometry(w, wallHeight, d);\n            const wMat = new THREE.MeshStandardMaterial({ color: 0x3e2723 }); // 木头色\n            const wMesh = new THREE.Mesh(wGeo, wMat);\n            wMesh.position.set(x, wallHeight/2, z);\n            wMesh.castShadow = true;\n            scene.add(wMesh);\n\n            // 物理\n            const wBody = new CANNON.Body({\n                mass: 0,\n                shape: new CANNON.Box(new CANNON.Vec3(w/2, wallHeight/2, d/2)),\n                position: new CANNON.Vec3(x, wallHeight/2, z)\n            });\n            world.addBody(wBody);\n        };\n\n        createWall(0, -TABLE_HEIGHT/2 - wallThickness/2, TABLE_WIDTH + wallThickness*2, wallThickness); // Top\n        createWall(0, TABLE_HEIGHT/2 + wallThickness/2, TABLE_WIDTH + wallThickness*2, wallThickness); // Bottom\n        createWall(-TABLE_WIDTH/2 - wallThickness/2, 0, wallThickness, TABLE_HEIGHT); // Left\n        createWall(TABLE_WIDTH/2 + wallThickness/2, 0, wallThickness, TABLE_HEIGHT); // Right\n    }\n\n    class Ball {\n        constructor(x, z, emotionKey) {\n            this.emotionKey = emotionKey;\n            this.mesh = null;\n            this.body = null;\n            this.uiElement = null;\n            this.barFill = null;\n            this.textElement = null;\n            \n            this.init(x, z);\n        }\n\n        init(x, z) {\n            // Three.js Mesh\n            const geometry = new THREE.SphereGeometry(BALL_RADIUS, 32, 32);\n            const material = new THREE.MeshStandardMaterial({ \n                color: EMOTIONS[this.emotionKey].color,\n                roughness: 0.4,\n                metalness: 0.3\n            });\n            this.mesh = new THREE.Mesh(geometry, material);\n            this.mesh.castShadow = true;\n            this.mesh.receiveShadow = true;\n            scene.add(this.mesh);\n\n            // Cannon.js Body\n            this.body = new CANNON.Body({\n                mass: 5, // 球的质量\n                shape: new CANNON.Sphere(BALL_RADIUS),\n                position: new CANNON.Vec3(x, BALL_RADIUS, z),\n                linearDamping: EMOTIONS[this.emotionKey].damping,\n                angularDamping: 0.5\n            });\n            \n            // 存储引用以便碰撞回调使用\n            this.body.userData = { ball: this };\n            \n            // 碰撞监听\n            this.body.addEventListener(\"collide\", (e) => {\n                this.handleCollision(e);\n            });\n\n            world.addBody(this.body);\n\n            // 创建UI\n            this.createUI();\n        }\n\n        createUI() {\n            const uiLayer = document.getElementById('ui-layer');\n            this.uiElement = document.createElement('div');\n            this.uiElement.className = 'ball-label';\n            \n            this.textElement = document.createElement('div');\n            this.textElement.className = 'emotion-text';\n            this.textElement.innerText = EMOTIONS[this.emotionKey].name;\n            this.uiElement.appendChild(this.textElement);\n\n            const barBg = document.createElement('div');\n            barBg.className = 'emotion-bar-bg';\n            this.barFill = document.createElement('div');\n            this.barFill.className = 'emotion-bar-fill';\n            this.barFill.style.backgroundColor = '#' + new THREE.Color(EMOTIONS[this.emotionKey].color).getHexString();\n            \n            barBg.appendChild(this.barFill);\n            this.uiElement.appendChild(barBg);\n            uiLayer.appendChild(this.uiElement);\n        }\n\n        setEmotion(key) {\n            if (this.emotionKey === key) return;\n            this.emotionKey = key;\n            \n            const config = EMOTIONS[key];\n            \n            // 更新视觉\n            this.mesh.material.color.setHex(config.color);\n            this.barFill.style.backgroundColor = '#' + new THREE.Color(config.color).getHexString();\n            this.textElement.innerText = config.name;\n\n            // 更新物理属性\n            this.body.linearDamping = config.damping;\n            // 注意：CannonJS 的 restitution 是在 ContactMaterial 中定义的，\n            // 为了简化，我们在碰撞处理逻辑中手动模拟 restitution 效果或者动态阻尼。\n        }\n\n        handleCollision(e) {\n            const otherBody = e.body;\n            if (!otherBody.userData || !otherBody.userData.ball) return; // 撞墙忽略\n            \n            const otherBall = otherBody.userData.ball;\n            \n            // 计算相对速度\n            const velocity = this.body.velocity;\n            const otherVelocity = otherBody.velocity;\n            const speed = velocity.length();\n            const otherSpeed = otherVelocity.length();\n\n            // 1. 兴奋球逻辑：反弹增强\n            if (this.emotionKey === 'EXCITED') {\n                // 给自己和对方一个额外的推力\n                const impulse = new CANNON.Vec3();\n                this.body.velocity.scale(1.2, impulse); // 简单的加速模拟高弹性\n                this.body.velocity.copy(impulse);\n            }\n\n            // 2. 情绪传染逻辑\n            // 规则：动能大（速度快）的球将情绪传染给动能小的球\n            // 只有当碰撞力度足够大时才传染\n            if (speed + otherSpeed > 5) {\n                if (speed > otherSpeed * 1.2) {\n                    // 我比你快很多，你被我传染\n                    otherBall.setEmotion(this.emotionKey);\n                } else if (otherSpeed > speed * 1.2) {\n                    // 你比我快很多，我被你传染\n                    this.setEmotion(otherBall.emotionKey);\n                }\n            }\n        }\n\n        update() {\n            // 同步位置\n            this.mesh.position.copy(this.body.position);\n            this.mesh.quaternion.copy(this.body.quaternion);\n\n            // 情绪特殊行为逻辑 (每帧执行)\n            const speed = this.body.velocity.length();\n\n            if (this.emotionKey === 'ANGRY' && speed > 0.5) {\n                // 愤怒：随机乱撞\n                const jitter = 20; \n                this.body.applyForce(new CANNON.Vec3(\n                    (Math.random() - 0.5) * jitter,\n                    0,\n                    (Math.random() - 0.5) * jitter\n                ), this.body.position);\n            }\n            else if (this.emotionKey === 'SAD') {\n                // 忧郁：如果速度慢，强制停止（因为阻尼大，但为了手感更好，加上阈值截断）\n                if (speed < 2 && speed > 0) {\n                    this.body.velocity.scale(0.9, this.body.velocity);\n                }\n            }\n\n            // 更新 UI 位置\n            this.updateUIPosition();\n        }\n\n        updateUIPosition() {\n            const tempV = new THREE.Vector3();\n            tempV.copy(this.mesh.position);\n            tempV.y += 2.5; // 偏移到球上方\n\n            // 投影到屏幕坐标\n            tempV.project(camera);\n\n            const x = (tempV.x * .5 + .5) * window.innerWidth;\n            const y = (tempV.y * -.5 + .5) * window.innerHeight;\n\n            // 简单的遮挡剔除（如果在相机后面则隐藏）\n            if(tempV.z > 1) {\n                this.uiElement.style.display = 'none';\n            } else {\n                this.uiElement.style.display = 'block';\n                this.uiElement.style.left = `${x}px`;\n                this.uiElement.style.top = `${y}px`;\n            }\n        }\n    }\n\n    function createBalls() {\n        // 母球 (白色/平静)\n        cueBall = new Ball(0, 15, 'NORMAL');\n        balls.push(cueBall);\n\n        // 目标球摆放 (三角形堆)\n        const startZ = -10;\n        let row = 0;\n        const keys = ['ANGRY', 'SAD', 'EXCITED'];\n        \n        for (let i = 0; i < 5; i++) {\n            for (let j = 0; j <= i; j++) {\n                const x = (j - i * 0.5) * (BALL_RADIUS * 2.1);\n                const z = startZ - i * (BALL_RADIUS * 1.8);\n                \n                // 随机分配初始情绪\n                const randomEmotion = keys[Math.floor(Math.random() * keys.length)];\n                balls.push(new Ball(x, z, randomEmotion));\n            }\n        }\n    }\n\n    // --- 交互逻辑 ---\n\n    function onMouseDown(event) {\n        event.preventDefault();\n        // 只有当球基本静止时才能击球\n        if (cueBall.body.velocity.length() > 0.5) return;\n\n        isDragging = true;\n        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;\n        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;\n        dragStart.set(event.clientX, event.clientY);\n        \n        // 禁用控制器以便拖拽\n        controls.enabled = false;\n    }\n\n    function onMouseMove(event) {\n        if (!isDragging) return;\n\n        // 计算拖拽力度线\n        const currentX = event.clientX;\n        const currentY = event.clientY;\n        \n        // 简单的屏幕空间映射到力度\n        const dx = currentX - dragStart.x;\n        const dy = currentY - dragStart.y;\n        \n        // 这里简化处理：基于相机视角的拖拽方向\n        // 我们只需要画一条线表示力度\n        const points = [];\n        const ballPos = cueBall.mesh.position;\n        points.push(ballPos.clone());\n        \n        // 射线检测地板，确定鼠标在3D空间的位置，用来决定击球方向\n        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;\n        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;\n        raycaster.setFromCamera(mouse, camera);\n        \n        // 创建一个临时平面用于射线检测\n        const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -1);\n        const target = new THREE.Vector3();\n        raycaster.ray.intersectPlane(plane, target);\n\n        if (target) {\n            const direction = new THREE.Vector3().subVectors(ballPos, target).normalize();\n            const dist = ballPos.distanceTo(target);\n            const drawEnd = ballPos.clone().add(direction.multiplyScalar(Math.min(dist, 10))); // 限制线长\n            points.push(drawEnd);\n            \n            lineGuide.geometry.setFromPoints(points);\n            lineGuide.visible = true;\n        }\n    }\n\n    function onMouseUp(event) {\n        if (!isDragging) return;\n        isDragging = false;\n        lineGuide.visible = false;\n        controls.enabled = true;\n\n        // 计算击球\n        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;\n        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;\n        raycaster.setFromCamera(mouse, camera);\n        const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -1);\n        const target = new THREE.Vector3();\n        raycaster.ray.intersectPlane(plane, target);\n\n        if (target) {\n            const direction = new THREE.Vector3().subVectors(cueBall.mesh.position, target);\n            // 力度限制\n            const forceMagnitude = Math.min(direction.length() * 500, 4000); \n            direction.normalize();\n            \n            // 在球心施加力\n            cueBall.body.applyForce(\n                new CANNON.Vec3(direction.x * forceMagnitude, 0, direction.z * forceMagnitude),\n                cueBall.body.position\n            );\n        }\n    }\n\n    function onWindowResize() {\n        camera.aspect = window.innerWidth / window.innerHeight;\n        camera.updateProjectionMatrix();\n        renderer.setSize(window.innerWidth, window.innerHeight);\n    }\n\n    // --- 游戏循环 ---\n    const timeStep = 1 / 60;\n    const clock = new THREE.Clock();\n\n    function animate() {\n        requestAnimationFrame(animate);\n\n        const dt = clock.getDelta();\n\n        // 更新物理\n        world.step(timeStep, dt, 3);\n\n        // 更新所有球\n        balls.forEach(ball => ball.update());\n\n        controls.update();\n        renderer.render(scene, camera);\n    }\n\n    // 启动\n    init();\n\n</script>\n</body>\n</html>\n```\n\n### 如何运行：\n1.  将上述代码复制并保存为 `game.html`。\n2.  双击文件或在浏览器中打开。\n3.  **操作方式**：\n    *   **击球**：鼠标点击并拖拽（类似愤怒的小鸟的操作方式），松开鼠标即可击打白球。\n    *   **视角**：按住鼠标左键（非击球时）旋转视角，滚轮缩放。\n\n### 核心逻辑解析：\n\n1.  **Ball 类 (The Ball Class)**:\n    *   这是一个封装类，同时包含 `THREE.Mesh` (视觉模型) 和 `CANNON.Body` (物理刚体)。\n    *   `update()` 方法每帧调用，将物理位置同步给视觉模型，并根据 `emotionKey` 执行特殊逻辑。\n\n2.  **情绪行为 (Emotion Behaviors)**:\n    *   **愤怒 (ANGRY)**: 在 `update()` 中，如果球有速度，会给它施加随机方向的微小力 (`body.applyForce`)，模拟“失控”和“抖动”。同时它的 `linearDamping` (线性阻尼) 很低。\n    *   **忧郁 (SAD)**: `linearDamping` 设置得很高 (0.8)，导致它在物理世界中像是在粘稠液体里运动。\n    *   **兴奋 (EXCITED)**: 在 `handleCollision` 中，如果检测到碰撞，会给球体一个额外的速度缩放 (`velocity.scale(1.2)`)，模拟超高弹性系数，使其越撞越快或反弹剧烈。\n\n3.  **情绪传染 (Contagion)**:\n    *   在 `handleCollision` 事件中，比较两个碰撞球体的速度。\n    *   如果 `speed > otherSpeed * 1.2` (快20%以上)，则判定为强势方，将强势方的 `emotionKey` 赋值给弱势方，并调用 `setEmotion` 更新颜色和物理属性。\n\n4.  **UI 同步**:\n    *   使用 `vector.project(camera)` 将球的3D坐标转换为屏幕2D坐标，实时更新 HTML DOM 元素的位置，实现血条跟随效果。",
    "model": "gemini-3-pro-preview",
    "usage": {
      "prompt_tokens": 144,
      "completion_tokens": 7828,
      "total_tokens": 7972
    },
    "timestamp": "2025-11-19 13:27:14"
  },
  "needs_screenshot": true,
  "needs_manual_test": true
}